WordPress.org

Make WordPress Core

Ticket #27112: 27112.diff

File 27112.diff, 156.1 KB (added by nacin, 7 years ago)
  • src/wp-admin/css/customize-widgets.css

     
     1.wp-full-overlay-sidebar {
     2        overflow: visible;
     3}
     4
     5/**
     6 * Hide all sidebar sections by default, only show them (via JS) once the
     7 * preview loads and we know whether the sidebars are used in the template.
     8 */
     9
     10.control-section[id^="accordion-section-sidebar-widgets-"],
     11.customize-control-sidebar_widgets label,
     12.customize-control-sidebar_widgets .hide-if-js {
     13        /* The link in .customize-control-sidebar_widgets .hide-if-js will fail if it ever gets used. */
     14        display:none;
     15}
     16
     17.customize-control-widget_form .widget-top {
     18        -webkit-transition: opacity 0.5s;
     19        transition: opacity 0.5s;
     20}
     21
     22.customize-control-widget_form:not(.widget-rendered) .widget-top {
     23        opacity: 0.5;
     24}
     25
     26
     27.customize-control-widget_form.is-live-previewable .widget-control-save {
     28        display: none;
     29}
     30
     31.customize-control-widget_form .spinner {
     32        display: inline;
     33        opacity: 0.0;
     34        -webkit-transition: opacity 0.1s;
     35        transition: opacity 0.1s;
     36}
     37.customize-control-widget_form.previewer-loading .spinner {
     38        opacity: 1.0;
     39}
     40
     41.customize-control-widget_form.widget-form-loading:not(.is-live-previewable) .widget-content {
     42        opacity: 0.7;
     43        pointer-events: none;
     44        -moz-user-select: none;
     45        -webkit-user-select: none;
     46        -ms-user-select: none;
     47        user-select: none;
     48}
     49
     50.customize-control-widget_form .widget {
     51        margin-bottom: 0;
     52}
     53
     54.customize-control-widget_form:not(.wide-widget-control) {
     55        /**
     56         * Prevent plugins (e.g. Widget Visibility in Jetpack) from forcing widget forms
     57         * to be wide and so overflow the customizer panel
     58         */
     59        left: auto !important;
     60        max-width: 100%;
     61}
     62.customize-control-widget_form.wide-widget-control .widget-inside {
     63        position: fixed;
     64        left: 299px;
     65        top: 25%;
     66        padding: 20px;
     67        border: 1px solid rgb(229, 229, 229);
     68        z-index: -1;
     69}
     70.customize-control-widget_form.wide-widget-control.collapsing .widget-inside {
     71        z-index: -2;
     72}
     73
     74.customize-control-widget_form.wide-widget-control .widget-top {
     75        -webkit-transition: background-color 0.4s;
     76        transition: background-color 0.4s;
     77}
     78.customize-control-widget_form.wide-widget-control.expanding .widget-top,
     79.customize-control-widget_form.wide-widget-control.expanded:not(.collapsing) .widget-top {
     80        background-color: rgb(227, 227, 227);
     81}
     82
     83.widget-inside {
     84        padding: 1px 10px 10px 10px;
     85        border-top: none;
     86        line-height: 16px;
     87}
     88
     89.widget-top {
     90        cursor: move;
     91}
     92
     93.customize-control-widget_form.expanded a.widget-action:after {
     94        content: "\f142";
     95}
     96
     97.customize-control-widget_form.wide-widget-control a.widget-action:after {
     98        content: "\f139";
     99}
     100
     101.customize-control-widget_form.wide-widget-control.expanded a.widget-action:after {
     102        content: "\f141";
     103}
     104
     105.widget-title-action {
     106        cursor: pointer;
     107}
     108
     109.customize-control-widget_form .widget .customize-control-title {
     110        cursor: move;
     111}
     112
     113/* @todo What does this do? */
     114.control-section.accordion-section.widget-customizer-highlighted > .accordion-section-title,
     115.customize-control-widget_form.widget-customizer-highlighted {
     116        outline: none;
     117        -webkit-box-shadow: 0 0 3px #ce0000;
     118        box-shadow: 0 0 3px #ce0000;
     119}
     120
     121#widget-customizer-control-templates {
     122        display: none;
     123}
     124
     125
     126/* MP6-compat */
     127#customize-theme-controls .accordion-section-content .widget {
     128        color: black;
     129}
     130
     131
     132/**
     133* Widget reordering styles
     134**/
     135
     136.reorder-toggle {
     137        float: right;
     138        padding: 5px 10px;
     139        margin-right: 10px;
     140        text-decoration: none;
     141        cursor: pointer;
     142        outline: none;
     143        -webkit-user-select: none;
     144        -moz-user-select: none;
     145        -ms-user-select: none;
     146        user-select: none;
     147}
     148.reorder-toggle:focus {
     149        outline: 1px dotted;
     150}
     151
     152.reorder-done,
     153.reordering .reorder {
     154        display: none;
     155}
     156
     157.reordering .reorder-done {
     158        display: block;
     159        color: #aa0000;
     160}
     161
     162#customize-theme-controls .reordering .add-new-widget {
     163        opacity: 0.2;
     164        pointer-events: none;
     165        cursor: not-allowed;
     166}
     167
     168#customize-theme-controls .widget-reorder-nav {
     169        display: none;
     170        float: right;
     171        background-color: #fafafa;
     172}
     173
     174.widget-reorder-nav span {
     175        position: relative;
     176        overflow: hidden;
     177        float: left;
     178        display: block;
     179        width: 33px; /* was 42px for mobile */
     180        height: 43px;
     181        color: #888;
     182        text-indent: -9999px;
     183        cursor: pointer;
     184        outline: none;
     185}
     186
     187.widget-reorder-nav span:before {
     188        display: inline-block;
     189        position: absolute;
     190        top: 0;
     191        right: 0;
     192        width: 100%;
     193        height: 100%;
     194        font: normal normal 20px/43px 'Genericons';
     195        text-align: center;
     196        text-indent: 0;
     197}
     198
     199.widget-reorder-nav span:hover,
     200.widget-reorder-nav span:focus {
     201        color: #444;
     202        background: #eee;
     203}
     204
     205.move-widget:before {
     206        content: '\f442';
     207}
     208
     209.move-widget-down:before {
     210        content: '\f431';
     211}
     212
     213.move-widget-up:before {
     214        content: '\f432';
     215}
     216
     217#customize-theme-controls .first-widget .move-widget-up,
     218#customize-theme-controls .last-widget .move-widget-down {
     219        color: #d5d5d5;
     220        cursor: default;
     221}
     222
     223#customize-theme-controls  .move-widget-area {
     224        display: none;
     225        background: #fff;
     226        border: 1px solid #dedede;
     227        border-top: none;
     228        cursor: auto;
     229}
     230
     231#customize-theme-controls .reordering .move-widget-area.active {
     232        display: block;
     233}
     234
     235#customize-theme-controls .move-widget-area .description {
     236        margin: 0;
     237        padding: 15px 20px;
     238        font-weight: 400;
     239}
     240
     241#customize-theme-controls .widget-area-select {
     242        margin: 0;
     243        padding: 0;
     244        list-style: none;
     245}
     246
     247#customize-theme-controls .widget-area-select li {
     248        position: relative;
     249        margin: 0;
     250        padding: 13px 15px 15px 42px;
     251        color: #555;
     252        border-top: 1px solid #eee;
     253        cursor: pointer;
     254        -webkit-user-select: none;
     255        -moz-user-select: none;
     256        -ms-user-select: none;
     257        user-select: none;
     258}
     259
     260#customize-theme-controls .widget-area-select li:before {
     261        display: none;
     262        content: '\f418';
     263        position: absolute;
     264        top: 10px;
     265        left: 10px;
     266        font-family: 'Genericons';
     267        font-size: 24px;
     268        line-height: 1;
     269}
     270
     271#customize-theme-controls .widget-area-select li:last-child {
     272        border-bottom: 1px solid #eee;
     273}
     274
     275#customize-theme-controls .widget-area-select .selected {
     276        color: #fff;
     277        text-shadow: 0 -1px 0 rgba(0,0,0,.4);
     278        border-top: 1px solid #207fa1;
     279        background: #2ea2cc;
     280}
     281
     282#customize-theme-controls .widget-area-select .selected:before {
     283        display: block;
     284}
     285
     286#customize-theme-controls .widget-area-select .selected:last-child {
     287        border-bottom: 1px solid #207fa1;
     288}
     289
     290#customize-theme-controls .move-widget-actions {
     291        text-align: right;
     292        padding: 12px;
     293}
     294
     295#customize-theme-controls .widget-area-select + li {
     296        border-top: 1px solid #207fa1;
     297}
     298
     299#customize-theme-controls .reordering .widget-title-action {
     300        display: none;
     301}
     302
     303#customize-theme-controls .reordering .widget-reorder-nav {
     304        display: block;
     305}
     306
     307
     308/**
     309 * Styles for new widget addition panel
     310 */
     311.wp-full-overlay-main {
     312        right: auto; /* this overrides a right: 0; which causes the preview to resize, I'd rather have it go off screen at the normal size. */
     313        width: 100%;
     314}
     315
     316.add-new-widget {
     317        cursor: pointer;
     318        float: right;
     319        -webkit-transition: all 0.2s;
     320        transition: all 0.2s;
     321        -webkit-user-select: none;
     322        -moz-user-select: none;
     323        -ms-user-select: none;
     324        user-select: none;
     325        -moz-outline: none;
     326        outline: none;
     327}
     328
     329.add-new-widget:before {
     330        content: "\f132";
     331        display: inline-block;
     332        position: relative;
     333                left: -2px;
     334                top: -1px;
     335        font: normal 16px/1 'dashicons';
     336        vertical-align: middle;
     337        -webkit-transition: all 0.2s;
     338        transition: all 0.2s;
     339        -webkit-font-smoothing: antialiased;
     340}
     341
     342body.adding-widget .add-new-widget,
     343body.adding-widget .add-new-widget:hover {
     344        background: #EEE;
     345        border-color: #999;
     346        color: #333;
     347        -webkit-box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
     348        box-shadow: inset 0 2px 5px -3px rgba(0, 0, 0, 0.5);
     349}
     350body.adding-widget .add-new-widget:before {
     351        -webkit-transform: rotate(45deg);
     352        -ms-transform: rotate(45deg);
     353        transform: rotate(45deg);
     354}
     355
     356#available-widgets .widget {
     357        position: static;
     358}
     359
     360/* override widgets admin page rules in wp-admin/css/wp-admin.css */
     361#widgets-left #available-widgets .widget {
     362        float: none !important;
     363        width: auto !important;
     364}
     365
     366#available-widgets {
     367        position: absolute;
     368        overflow: auto;
     369        top: 0;
     370        bottom: 0;
     371        left: -301px;
     372        width: 300px;
     373        margin: 0;
     374        z-index: 1;
     375        background: #fff;
     376        -webkit-transition: all 0.2s;
     377        transition: all 0.2s;
     378        border-right: 1px solid #dddddd;
     379}
     380
     381#available-widgets-filter {
     382        padding: 8px 17px 7px 13px;
     383        border-bottom: 1px solid #e4e4e4;
     384        -webkit-box-sizing: border-box;
     385        -moz-box-sizing: border-box;
     386        box-sizing: border-box;
     387}
     388
     389#available-widgets-filter input {
     390        padding: 5px 10px 2px 10px;
     391        width: 100%;
     392}
     393
     394#available-widgets .widget-tpl {
     395        position: relative;
     396        padding: 20px 15px 20px 60px;
     397        border-bottom: 1px solid #e4e4e4;
     398        cursor: pointer;
     399}
     400
     401#available-widgets .widget-tpl:hover,
     402#available-widgets .widget-tpl.selected {
     403        background: #fafafa;
     404}
     405
     406#available-widgets .widget-top,
     407#available-widgets .widget-top:hover {
     408        border: none;
     409        background: transparent;
     410        -webkit-box-shadow: none;
     411        box-shadow: none;
     412}
     413
     414#available-widgets .widget-title h4 {
     415        padding: 0 0 5px;
     416        font-size: 14px;
     417}
     418
     419#available-widgets .widget .widget-description {
     420        padding: 0;
     421        color: #777;
     422}
     423
     424#customize-preview {
     425        -webkit-transition: all 0.2s;
     426        transition: all 0.2s;
     427}
     428
     429body.adding-widget #available-widgets {
     430        left: 0;
     431}
     432
     433body.adding-widget .wp-full-overlay-main {
     434        left: 300px;
     435}
     436
     437body.adding-widget #customize-preview {
     438        opacity: 0.4;
     439}
     440
     441
     442/** Widget Icon styling **
     443
     444* No plurals in naming.
     445* Ordered from lowest to highest specificity.
     446
     447**/
     448#available-widgets .widget-title {
     449        position: relative;
     450}
     451
     452#available-widgets .widget-title:before {
     453        content:"\f132";
     454        position: absolute;
     455        top: -3px;
     456        right: 100%;
     457        margin-right: 20px;
     458        width: 20px;
     459        height: 20px;
     460        color: #333;
     461        font: normal 20px/1 'dashicons', 'widgeticons';
     462        text-align: center;
     463        -webkit-border-radius: 2px;
     464        border-radius: 2px;
     465        -webkit-box-sizing: border-box;
     466        -moz-box-sizing: border-box;
     467        box-sizing: border-box;
     468        -webkit-font-smoothing: antialiased;
     469}
     470
     471/* smiley */
     472#available-widgets [class*="easy"] .widget-title:before { content: "\f328"; top: -4px; }
     473
     474/* star-filled */
     475#available-widgets [class*="super"] .widget-title:before,
     476#available-widgets [class*="like"] .widget-title:before { content: "\f155"; top: -4px; }
     477
     478/* wordpress */
     479#available-widgets [class*="meta"] .widget-title:before { content: "\f120"; }
     480
     481/* archive-box */
     482#available-widgets [class*="archives"] .widget-title:before { content: "\f483"; top: -4px; }
     483
     484/* category */
     485#available-widgets [class*="categor"] .widget-title:before { content: "\f318"; top: -4px; }
     486
     487/* comments */
     488#available-widgets [class*="comment"] .widget-title:before,
     489#available-widgets [class*="testimonial"] .widget-title:before,
     490#available-widgets [class*="chat"] .widget-title:before { content: "\f101"; }
     491
     492/* post */
     493#available-widgets [class*="post"] .widget-title:before { content: "\f109"; }
     494
     495/* admin-page */
     496#available-widgets [class*="page"] .widget-title:before { content: "\f105"; }
     497
     498/* text */
     499#available-widgets [class*="text"] .widget-title:before { content: "\f480"; }
     500
     501/* links */
     502#available-widgets [class*="link"] .widget-title:before { content: "\f103"; }
     503
     504/* search */
     505#available-widgets [class*="search"] .widget-title:before { content: "\f179"; }
     506
     507/* menu */
     508#available-widgets [class*="menu"] .widget-title:before,
     509#available-widgets [class*="nav"] .widget-title:before { content: "\f333"; }
     510
     511/* tag-cloud */
     512#available-widgets [class*="tag"] .widget-title:before { content: "\f481"; }
     513
     514/* rss */
     515#available-widgets [class*="rss"] .widget-title:before { content: "\f303"; top: -6px; }
     516
     517/* calendar */
     518#available-widgets [class*="event"] .widget-title:before,
     519#available-widgets [class*="calendar"] .widget-title:before { content: "\f145"; top: -4px;}
     520
     521/* format-image */
     522#available-widgets [class*="image"] .widget-title:before,
     523#available-widgets [class*="photo"] .widget-title:before,
     524#available-widgets [class*="slide"] .widget-title:before,
     525#available-widgets [class*="instagram"] .widget-title:before { content: "\f128"; }
     526
     527/* format-gallery */
     528#available-widgets [class*="album"] .widget-title:before,
     529#available-widgets [class*="galler"] .widget-title:before { content: "\f161"; }
     530
     531/* format-video */
     532#available-widgets [class*="video"] .widget-title:before,
     533#available-widgets [class*="tube"] .widget-title:before { content: "\f126"; }
     534
     535/* format-audio */
     536#available-widgets [class*="music"] .widget-title:before,
     537#available-widgets [class*="radio"] .widget-title:before,
     538#available-widgets [class*="audio"] .widget-title:before { content: "\f127"; }
     539
     540/* admin-users */
     541#available-widgets [class*="login"] .widget-title:before,
     542#available-widgets [class*="user"] .widget-title:before,
     543#available-widgets [class*="member"] .widget-title:before,
     544#available-widgets [class*="avatar"] .widget-title:before,
     545#available-widgets [class*="subscriber"] .widget-title:before,
     546#available-widgets [class*="profile"] .widget-title:before,
     547#available-widgets [class*="grofile"] .widget-title:before { content: "\f110"; }
     548
     549/* cart */
     550#available-widgets [class*="commerce"] .widget-title:before,
     551#available-widgets [class*="shop"] .widget-title:before,
     552#available-widgets [class*="cart"] .widget-title:before { content: "\f174"; top: -4px; }
     553
     554/* shield */
     555#available-widgets [class*="secur"] .widget-title:before,
     556#available-widgets [class*="firewall"] .widget-title:before { content: "\f332"; }
     557
     558/* chart-bar */
     559#available-widgets [class*="analytic"] .widget-title:before,
     560#available-widgets [class*="stat"] .widget-title:before,
     561#available-widgets [class*="poll"] .widget-title:before { content: "\f185"; }
     562
     563/* feedback */
     564#available-widgets [class*="form"] .widget-title:before { content: "\f175"; }
     565
     566/* email-alt */
     567#available-widgets [class*="subscribe"] .widget-title:before,
     568#available-widgets [class*="news"] .widget-title:before,
     569#available-widgets [class*="contact"] .widget-title:before,
     570#available-widgets [class*="mail"] .widget-title:before { content: "\f466"; }
     571
     572/* share */
     573#available-widgets [class*="share"] .widget-title:before,
     574#available-widgets [class*="socia"] .widget-title:before { content: "\f237"; }
     575
     576/* translation */
     577#available-widgets [class*="lang"] .widget-title:before,
     578#available-widgets [class*="translat"] .widget-title:before { content: "\f326"; }
     579
     580/* location-alt */
     581#available-widgets [class*="locat"] .widget-title:before,
     582#available-widgets [class*="map"] .widget-title:before { content: "\f231"; }
     583
     584/* download */
     585#available-widgets [class*="download"] .widget-title:before { content: "\f316"; }
     586
     587/* cloud */
     588#available-widgets [class*="weather"] .widget-title:before { content: "\f176"; top: -4px;}
     589
     590/* facebook */
     591#available-widgets [class*="facebook"] .widget-title:before { content: "\f304"; }
     592
     593/* twitter */
     594#available-widgets [class*="tweet"] .widget-title:before,
     595#available-widgets [class*="twitter"] .widget-title:before { content: "\f301"; }
     596
     597
     598@media screen and (max-height: 700px) and (min-width: 981px) {
     599        .customize-control {
     600                margin-bottom: 0;
     601        }
     602        .widget-top {
     603                -webkit-box-shadow: none;
     604                box-shadow: none;
     605                margin-top: -1px;
     606        }
     607        .widget-top:hover {
     608                position: relative;
     609                z-index: 1;
     610        }
     611        .last-widget {
     612                margin-bottom: 15px;
     613        }
     614        .widget-title h4 {
     615                padding: 13px 15px;
     616        }
     617        .widget-top a.widget-action:after {
     618                padding-top: 9px;
     619        }
     620        .widget-reorder-nav span {
     621                height: 39px;
     622        }
     623        .widget-reorder-nav span:before {
     624                line-height: 39px;
     625        }
     626        #customize-theme-controls .widget-area-select li {
     627                padding: 9px 15px 11px 42px;
     628        }
     629        #customize-theme-controls .widget-area-select li:before {
     630                top: 6px;
     631        }
     632}
  • src/wp-admin/js/customize-widgets.js

     
     1/*global wp, Backbone, _, jQuery, WidgetCustomizer_exports */
     2/*exported WidgetCustomizer */
     3var WidgetCustomizer = ( function ($) {
     4        'use strict';
     5
     6        var customize = wp.customize;
     7        var self = {
     8                update_widget_ajax_action: null,
     9                update_widget_nonce_value: null,
     10                update_widget_nonce_post_key: null,
     11                i18n: {
     12                        save_btn_label: '',
     13                        save_btn_tooltip: '',
     14                        remove_btn_label: '',
     15                        remove_btn_tooltip: ''
     16                },
     17                available_widgets: [], // available widgets for instantiating
     18                registered_widgets: [], // all widgets registered
     19                active_sidebar_control: null,
     20                sidebars_eligible_for_post_message: {},
     21                widgets_eligible_for_post_message: {},
     22                current_theme_supports: false,
     23                previewer: null,
     24                saved_widget_ids: {},
     25                registered_sidebars: [],
     26                tpl: {
     27                        move_widget_area: '',
     28                        widget_reorder_nav: ''
     29                }
     30        };
     31        $.extend( self, WidgetCustomizer_exports );
     32
     33        // Lots of widgets expect this old ajaxurl global to be available
     34        if ( typeof window.ajaxurl === 'undefined' ) {
     35                window.ajaxurl = wp.ajax.settings.url;
     36        }
     37
     38        // Unfortunately many widgets try to look for instances under div#widgets-right,
     39        // so we have to add that ID to a container div in the customizer for compat
     40        $( '#customize-theme-controls' ).closest( 'div:not([id])' ).attr( 'id', 'widgets-right' );
     41
     42        /**
     43         * Set up model
     44         */
     45        var Widget = self.Widget = Backbone.Model.extend( {
     46                id: null,
     47                temp_id: null,
     48                classname: null,
     49                control_tpl: null,
     50                description: null,
     51                is_disabled: null,
     52                is_multi: null,
     53                multi_number: null,
     54                name: null,
     55                id_base: null,
     56                transport: 'refresh',
     57                params: [],
     58                width: null,
     59                height: null
     60        } );
     61        var WidgetCollection = self.WidgetCollection = Backbone.Collection.extend( {
     62                model: Widget
     63        } );
     64        self.available_widgets = new WidgetCollection( self.available_widgets );
     65
     66        var Sidebar = self.Sidebar = Backbone.Model.extend( {
     67                after_title: null,
     68                after_widget: null,
     69                before_title: null,
     70                before_widget: null,
     71                'class': null,
     72                description: null,
     73                id: null,
     74                name: null,
     75                is_rendered: false
     76        } );
     77        var SidebarCollection = self.SidebarCollection = Backbone.Collection.extend( {
     78                model: Sidebar
     79        } );
     80        self.registered_sidebars = new SidebarCollection( self.registered_sidebars );
     81
     82        /**
     83         * On DOM ready, initialize some meta functionality independent of specific
     84         * customizer controls.
     85         */
     86        self.init = function () {
     87                this.showFirstSidebarIfRequested();
     88                this.availableWidgetsPanel.setup();
     89        };
     90        wp.customize.bind( 'ready', function () {
     91                self.init();
     92        } );
     93
     94        /**
     95         * Listen for updates to which sidebars are rendered in the preview and toggle
     96         * the customizer sections accordingly.
     97         */
     98        self.showFirstSidebarIfRequested = function () {
     99                if ( ! /widget-customizer=open/.test( location.search ) ) {
     100                        return;
     101                }
     102
     103                var show_first_visible_sidebar = function () {
     104                        self.registered_sidebars.off( 'change:is_rendered', show_first_visible_sidebar );
     105                        var first_rendered_sidebar = self.registered_sidebars.find( function ( sidebar ) {
     106                                return sidebar.get( 'is_rendered' );
     107                        } );
     108                        if ( ! first_rendered_sidebar ) {
     109                                return;
     110                        }
     111                        var section = $( '#accordion-section-sidebar-widgets-' + first_rendered_sidebar.get( 'id' ) );
     112                        if ( ! section.hasClass( 'open' ) ) {
     113                                section.find( '.accordion-section-title' ).trigger( 'click' );
     114                        }
     115                        section[0].scrollIntoView();
     116                };
     117                show_first_visible_sidebar = _.debounce( show_first_visible_sidebar, 100 ); // so only fires when all updated at end
     118                self.registered_sidebars.on( 'change:is_rendered', show_first_visible_sidebar );
     119        };
     120
     121        /**
     122         * Sidebar Widgets control
     123         * Note that 'sidebar_widgets' must match the Sidebar_Widgets_WP_Customize_Control::$type
     124         */
     125        customize.controlConstructor.sidebar_widgets = customize.Control.extend( {
     126
     127                /**
     128                 * Set up the control
     129                 */
     130                ready: function() {
     131                        var control = this;
     132                        control.control_section = control.container.closest( '.control-section' );
     133                        control.section_content = control.container.closest( '.accordion-section-content' );
     134                        control._setupModel();
     135                        control._setupSortable();
     136                        control._setupAddition();
     137                        control._applyCardinalOrderClassNames();
     138                },
     139
     140                /**
     141                 * Update ordering of widget control forms when the setting is updated
     142                 */
     143                _setupModel: function() {
     144                        var control = this;
     145                        var registered_sidebar = self.registered_sidebars.get( control.params.sidebar_id );
     146
     147                        control.setting.bind( function( new_widget_ids, old_widget_ids ) {
     148                                var removed_widget_ids = _( old_widget_ids ).difference( new_widget_ids );
     149
     150                                // Filter out any persistent widget_ids for widgets which have been deactivated
     151                                new_widget_ids = _( new_widget_ids ).filter( function ( new_widget_id ) {
     152                                        var parsed_widget_id = parse_widget_id( new_widget_id );
     153                                        return !! self.available_widgets.findWhere( { id_base: parsed_widget_id.id_base } );
     154                                } );
     155
     156                                var widget_form_controls = _( new_widget_ids ).map( function ( widget_id ) {
     157                                        var widget_form_control = self.getWidgetFormControlForWidget( widget_id );
     158                                        if ( ! widget_form_control ) {
     159                                                widget_form_control = control.addWidget( widget_id );
     160                                        }
     161                                        return widget_form_control;
     162                                } );
     163
     164                                // Sort widget controls to their new positions
     165                                widget_form_controls.sort( function ( a, b ) {
     166                                        var a_index = new_widget_ids.indexOf( a.params.widget_id );
     167                                        var b_index = new_widget_ids.indexOf( b.params.widget_id );
     168                                        if ( a_index === b_index ) {
     169                                                return 0;
     170                                        }
     171                                        return a_index < b_index ? -1 : 1;
     172                                } );
     173
     174                                var sidebar_widgets_add_control = control.section_content.find( '.customize-control-sidebar_widgets' );
     175
     176                                // Append the controls to put them in the right order
     177                                var final_control_containers = _( widget_form_controls ).map( function( widget_form_controls ) {
     178                                        return widget_form_controls.container[0];
     179                                } );
     180
     181                                // Re-sort widget form controls (including widgets form other sidebars newly moved here)
     182                                sidebar_widgets_add_control.before( final_control_containers );
     183                                control._applyCardinalOrderClassNames();
     184
     185                                // If the widget was dragged into the sidebar, make sure the sidebar_id param is updated
     186                                _( widget_form_controls ).each( function ( widget_form_control ) {
     187                                        widget_form_control.params.sidebar_id = control.params.sidebar_id;
     188                                } );
     189
     190                                // Cleanup after widget removal
     191                                _( removed_widget_ids ).each( function ( removed_widget_id ) {
     192
     193                                        // Using setTimeout so that when moving a widget to another sidebar, the other sidebars_widgets settings get a chance to update
     194                                        setTimeout( function () {
     195                                                var is_present_in_another_sidebar = false;
     196
     197                                                // Check if the widget is in another sidebar
     198                                                wp.customize.each( function ( other_setting ) {
     199                                                        if ( other_setting.id === control.setting.id || 0 !== other_setting.id.indexOf( 'sidebars_widgets[' ) || other_setting.id === 'sidebars_widgets[wp_inactive_widgets]' ) {
     200                                                                return;
     201                                                        }
     202                                                        var other_sidebar_widgets = other_setting();
     203                                                        var i = other_sidebar_widgets.indexOf( removed_widget_id );
     204                                                        if ( -1 !== i ) {
     205                                                                is_present_in_another_sidebar = true;
     206                                                        }
     207                                                } );
     208
     209                                                // If the widget is present in another sidebar, abort!
     210                                                if ( is_present_in_another_sidebar ) {
     211                                                        return;
     212                                                }
     213
     214                                                var removed_control = self.getWidgetFormControlForWidget( removed_widget_id );
     215
     216                                                // Detect if widget control was dragged to another sidebar
     217                                                var was_dragged_to_another_sidebar = (
     218                                                        removed_control &&
     219                                                        $.contains( document, removed_control.container[0] ) &&
     220                                                        ! $.contains( control.section_content[0], removed_control.container[0] )
     221                                                );
     222
     223                                                // Delete any widget form controls for removed widgets
     224                                                if ( removed_control && ! was_dragged_to_another_sidebar ) {
     225                                                        wp.customize.control.remove( removed_control.id );
     226                                                        removed_control.container.remove();
     227                                                }
     228
     229                                                // Move widget to inactive widgets sidebar (move it to trash) if has been previously saved
     230                                                // This prevents the inactive widgets sidebar from overflowing with throwaway widgets
     231                                                if ( self.saved_widget_ids[removed_widget_id] ) {
     232                                                        var inactive_widgets = wp.customize.value( 'sidebars_widgets[wp_inactive_widgets]' )().slice();
     233                                                        inactive_widgets.push( removed_widget_id );
     234                                                        wp.customize.value( 'sidebars_widgets[wp_inactive_widgets]' )( _( inactive_widgets ).unique() );
     235                                                }
     236
     237                                                // Make old single widget available for adding again
     238                                                var removed_id_base = parse_widget_id( removed_widget_id ).id_base;
     239                                                var widget = self.available_widgets.findWhere( { id_base: removed_id_base } );
     240                                                if ( widget && ! widget.get( 'is_multi' ) ) {
     241                                                        widget.set( 'is_disabled', false );
     242                                                }
     243                                        } );
     244
     245                                } );
     246                        } );
     247
     248                        // Update the model with whether or not the sidebar is rendered
     249                        self.previewer.bind( 'rendered-sidebars', function ( rendered_sidebars ) {
     250                                var is_rendered = !! rendered_sidebars[control.params.sidebar_id];
     251                                registered_sidebar.set( 'is_rendered', is_rendered );
     252                        } );
     253
     254                        // Show the sidebar section when it becomes visible
     255                        registered_sidebar.on( 'change:is_rendered', function ( ) {
     256                                var section_selector = '#accordion-section-sidebar-widgets-' + this.get( 'id' );
     257                                var section = $( section_selector );
     258                                if ( this.get( 'is_rendered' ) ) {
     259                                        section.stop().slideDown( function () {
     260                                                $( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow
     261                                        } );
     262                                } else {
     263                                        // Make sure that hidden sections get closed first
     264                                        if ( section.hasClass( 'open' ) ) {
     265                                                // it would be nice if accordionSwitch() in accordion.js was public
     266                                                section.find( '.accordion-section-title' ).trigger( 'click' );
     267                                        }
     268                                        section.stop().slideUp();
     269                                }
     270                        } );
     271                },
     272
     273                /**
     274                 * Allow widgets in sidebar to be re-ordered, and for the order to be previewed
     275                 */
     276                _setupSortable: function () {
     277                        var control = this;
     278                        control.is_reordering = false;
     279
     280                        /**
     281                         * Update widget order setting when controls are re-ordered
     282                         */
     283                        control.section_content.sortable( {
     284                                items: '> .customize-control-widget_form',
     285                                handle: '.widget-top',
     286                                axis: 'y',
     287                                connectWith: '.accordion-section-content:has(.customize-control-sidebar_widgets)',
     288                                update: function () {
     289                                        var widget_container_ids = control.section_content.sortable( 'toArray' );
     290                                        var widget_ids = $.map( widget_container_ids, function ( widget_container_id ) {
     291                                                return $( '#' + widget_container_id ).find( ':input[name=widget-id]' ).val();
     292                                        } );
     293                                        control.setting( widget_ids );
     294                                }
     295                        } );
     296
     297                        /**
     298                         * Expand other customizer sidebar section when dragging a control widget over it,
     299                         * allowing the control to be dropped into another section
     300                         */
     301                        control.control_section.find( '.accordion-section-title' ).droppable( {
     302                                accept: '.customize-control-widget_form',
     303                                over: function () {
     304                                        if ( ! control.control_section.hasClass( 'open' ) ) {
     305                                                control.control_section.addClass( 'open' );
     306                                                control.section_content.toggle( false ).slideToggle( 150, function () {
     307                                                        control.section_content.sortable( 'refreshPositions' );
     308                                                } );
     309                                        }
     310                                }
     311                        } );
     312
     313                        /**
     314                         * Keyboard-accessible reordering
     315                         */
     316                        control.container.find( '.reorder-toggle' ).on( 'click keydown', function( event ) {
     317                                if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
     318                                        return;
     319                                }
     320
     321                                control.toggleReordering( ! control.is_reordering );
     322                        } );
     323                },
     324
     325                /**
     326                 * Set up UI for adding a new widget
     327                 */
     328                _setupAddition: function () {
     329                        var control = this;
     330
     331                        control.container.find( '.add-new-widget' ).on( 'click keydown', function( event ) {
     332                                if ( event.type === 'keydown' && ! ( event.which === 13 || event.which === 32 ) ) { // Enter or Spacebar
     333                                        return;
     334                                }
     335
     336                                if ( control.section_content.hasClass( 'reordering' ) ) {
     337                                        return;
     338                                }
     339
     340                                // @todo Use an control.is_adding state
     341                                if ( ! $( 'body' ).hasClass( 'adding-widget' ) ) {
     342                                        self.availableWidgetsPanel.open( control );
     343                                } else {
     344                                        self.availableWidgetsPanel.close();
     345                                }
     346                        } );
     347                },
     348
     349                /**
     350                 * Add classes to the widget_form controls to assist with styling
     351                 */
     352                _applyCardinalOrderClassNames: function () {
     353                        var control = this;
     354                        control.section_content.find( '.customize-control-widget_form' )
     355                                .removeClass( 'first-widget' )
     356                                .removeClass( 'last-widget' )
     357                                .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 );
     358
     359                        control.section_content.find( '.customize-control-widget_form:first' )
     360                                .addClass( 'first-widget' )
     361                                .find( '.move-widget-up' ).prop( 'tabIndex', -1 );
     362                        control.section_content.find( '.customize-control-widget_form:last' )
     363                                .addClass( 'last-widget' )
     364                                .find( '.move-widget-down' ).prop( 'tabIndex', -1 );
     365                },
     366
     367
     368                /***********************************************************************
     369                 * Begin public API methods
     370                 **********************************************************************/
     371
     372                /**
     373                 * Enable/disable the reordering UI
     374                 *
     375                 * @param {Boolean} toggle to enable/disable reordering
     376                 */
     377                toggleReordering: function ( toggle ) {
     378                        var control = this;
     379                        toggle = Boolean( toggle );
     380                        if ( toggle === control.section_content.hasClass( 'reordering' ) ) {
     381                                return;
     382                        }
     383
     384                        control.is_reordering = toggle;
     385                        control.section_content.toggleClass( 'reordering', toggle );
     386
     387                        if ( toggle ) {
     388                                _( control.getWidgetFormControls() ).each( function ( form_control ) {
     389                                        form_control.collapseForm();
     390                                } );
     391                        }
     392                },
     393
     394                /**
     395                 * @return {wp.customize.controlConstructor.widget_form[]}
     396                 */
     397                getWidgetFormControls: function () {
     398                        var control = this;
     399                        var form_controls = _( control.setting() ).map( function ( widget_id ) {
     400                                var setting_id = widget_id_to_setting_id( widget_id );
     401                                var form_control = customize.control( setting_id );
     402                                if ( ! form_control ) {
     403                                        throw new Error( 'Unable to find widget_form control for ' + widget_id );
     404                                }
     405                                return form_control;
     406                        } );
     407                        return form_controls;
     408                },
     409
     410                /**
     411                 * @param {string} widget_id or an id_base for adding a previously non-existing widget
     412                 * @returns {object} widget_form control instance
     413                 */
     414                addWidget: function ( widget_id ) {
     415                        var control = this;
     416                        var parsed_widget_id = parse_widget_id( widget_id );
     417                        var widget_number = parsed_widget_id.number;
     418                        var widget_id_base = parsed_widget_id.id_base;
     419                        var widget = self.available_widgets.findWhere( {id_base: widget_id_base} );
     420                        if ( ! widget ) {
     421                                throw new Error( 'Widget unexpectedly not found.' );
     422                        }
     423                        if ( widget_number && ! widget.get( 'is_multi' ) ) {
     424                                throw new Error( 'Did not expect a widget number to be supplied for a non-multi widget' );
     425                        }
     426
     427                        // Set up new multi widget
     428                        if ( widget.get( 'is_multi' ) && ! widget_number ) {
     429                                widget.set( 'multi_number', widget.get( 'multi_number' ) + 1 );
     430                                widget_number = widget.get( 'multi_number' );
     431                        }
     432
     433                        var control_html = $( '#widget-tpl-' + widget.get( 'id' ) ).html();
     434                        if ( widget.get( 'is_multi' ) ) {
     435                                control_html = control_html.replace( /<[^<>]+>/g, function ( m ) {
     436                                        return m.replace( /__i__|%i%/g, widget_number );
     437                                } );
     438                        } else {
     439                                widget.set( 'is_disabled', true ); // Prevent single widget from being added again now
     440                        }
     441
     442                        var customize_control_type = 'widget_form';
     443                        var customize_control = $( '<li></li>' );
     444                        customize_control.addClass( 'customize-control' );
     445                        customize_control.addClass( 'customize-control-' + customize_control_type );
     446                        customize_control.append( $( control_html ) );
     447                        customize_control.find( '> .widget-icon' ).remove();
     448                        if ( widget.get( 'is_multi' ) ) {
     449                                customize_control.find( 'input[name="widget_number"]' ).val( widget_number );
     450                                customize_control.find( 'input[name="multi_number"]' ).val( widget_number );
     451                        }
     452                        widget_id = customize_control.find( '[name="widget-id"]' ).val();
     453                        customize_control.hide(); // to be slid-down below
     454
     455                        var setting_id = 'widget_' + widget.get( 'id_base' );
     456                        if ( widget.get( 'is_multi' ) ) {
     457                                setting_id += '[' + widget_number + ']';
     458                        }
     459                        customize_control.attr( 'id', 'customize-control-' + setting_id.replace( /\]/g, '' ).replace( /\[/g, '-' ) );
     460
     461                        control.container.after( customize_control );
     462
     463                        // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget)
     464                        var is_existing_widget = wp.customize.has( setting_id );
     465                        if ( ! is_existing_widget ) {
     466                                var setting_args = {
     467                                        transport: 'refresh', // preview window will opt-in to postMessage if available
     468                                        previewer: control.setting.previewer
     469                                };
     470                                var sidebar_can_live_preview = self.getPreviewWindow().WidgetCustomizerPreview.sidebarCanLivePreview( control.params.sidebar_id );
     471                                var widget_can_live_preview = !! self.widgets_eligible_for_post_message[ widget_id_base ];
     472                                if ( self.current_theme_supports && sidebar_can_live_preview && widget_can_live_preview ) {
     473                                        setting_args.transport = 'postMessage';
     474                                }
     475                                wp.customize.create( setting_id, setting_id, {}, setting_args );
     476                        }
     477
     478                        var Constructor = wp.customize.controlConstructor[customize_control_type];
     479                        var widget_form_control = new Constructor( setting_id, {
     480                                params: {
     481                                        settings: {
     482                                                'default': setting_id
     483                                        },
     484                                        sidebar_id: control.params.sidebar_id,
     485                                        widget_id: widget_id,
     486                                        widget_id_base: widget.get( 'id_base' ),
     487                                        type: customize_control_type,
     488                                        is_new: ! is_existing_widget,
     489                                        width: widget.get( 'width' ),
     490                                        height: widget.get( 'height' ),
     491                                        is_wide: widget.get( 'is_wide' ),
     492                                        is_live_previewable: widget.get( 'is_live_previewable' )
     493                                },
     494                                previewer: control.setting.previewer
     495                        } );
     496                        wp.customize.control.add( setting_id, widget_form_control );
     497
     498                        // Make sure widget is removed from the other sidebars
     499                        wp.customize.each( function ( other_setting ) {
     500                                if ( other_setting.id === control.setting.id ) {
     501                                        return;
     502                                }
     503                                if ( 0 !== other_setting.id.indexOf( 'sidebars_widgets[' ) ) {
     504                                        return;
     505                                }
     506                                var other_sidebar_widgets = other_setting().slice();
     507                                var i = other_sidebar_widgets.indexOf( widget_id );
     508                                if ( -1 !== i ) {
     509                                        other_sidebar_widgets.splice( i );
     510                                        other_setting( other_sidebar_widgets );
     511                                }
     512                        } );
     513
     514                        // Add widget to this sidebar
     515                        var sidebar_widgets = control.setting().slice();
     516                        if ( -1 === sidebar_widgets.indexOf( widget_id ) ) {
     517                                sidebar_widgets.push( widget_id );
     518                                control.setting( sidebar_widgets );
     519                        }
     520
     521                        customize_control.slideDown( function () {
     522                                if ( is_existing_widget ) {
     523                                        widget_form_control.expandForm();
     524                                        widget_form_control.updateWidget( {
     525                                                instance: widget_form_control.setting(),
     526                                                complete: function ( error ) {
     527                                                        if ( error ) {
     528                                                                throw error;
     529                                                        }
     530                                                        widget_form_control.focus();
     531                                                }
     532                                        } );
     533                                } else {
     534                                        widget_form_control.focus();
     535                                }
     536                        } );
     537
     538                        return widget_form_control;
     539                }
     540
     541        } );
     542
     543        /**
     544         * Widget Form control
     545         * Note that 'widget_form' must match the Widget_Form_WP_Customize_Control::$type
     546         */
     547        customize.controlConstructor.widget_form = customize.Control.extend( {
     548
     549                /**
     550                 * Set up the control
     551                 */
     552                ready: function() {
     553                        var control = this;
     554                        control._setupModel();
     555                        control._setupWideWidget();
     556                        control._setupControlToggle();
     557                        control._setupWidgetTitle();
     558                        control._setupReorderUI();
     559                        control._setupHighlightEffects();
     560                        control._setupUpdateUI();
     561                        control._setupRemoveUI();
     562                        control.hook( 'init' );
     563                },
     564
     565                /**
     566                 * Hooks for widgets to support living in the customizer control
     567                 */
     568                hooks: {
     569                        _default: {},
     570                        rss: {
     571                                formUpdated: function ( serialized_form ) {
     572                                        var control = this;
     573                                        var old_widget_error = control.container.find( '.widget-error:first' );
     574                                        var new_widget_error = serialized_form.find( '.widget-error:first' );
     575                                        if ( old_widget_error.length && new_widget_error.length ) {
     576                                                old_widget_error.replaceWith( new_widget_error );
     577                                        } else if ( old_widget_error.length ) {
     578                                                old_widget_error.remove();
     579                                        } else if ( new_widget_error.length ) {
     580                                                control.container.find( '.widget-content' ).prepend( new_widget_error );
     581                                        }
     582                                }
     583                        }
     584                },
     585
     586                /**
     587                 * Trigger an 'action' which a specific widget type can handle
     588                 *
     589                 * @param name
     590                 */
     591                hook: function ( name ) {
     592                        var args = Array.prototype.slice.call( arguments, 1 );
     593                        var handler;
     594                        if ( this.hooks[this.params.widget_id_base] && this.hooks[this.params.widget_id_base][name] ) {
     595                                handler = this.hooks[this.params.widget_id_base][name];
     596                        } else if ( this.hooks._default[name] ) {
     597                                handler = this.hooks._default[name];
     598                        }
     599                        if ( handler ) {
     600                                handler.apply( this, args );
     601                        }
     602                },
     603
     604                /**
     605                 * Handle changes to the setting
     606                 */
     607                _setupModel: function () {
     608                        var control = this;
     609
     610                        // Remember saved widgets so we know which to trash (move to inactive widgets sidebar)
     611                        var remember_saved_widget_id = function () {
     612                                self.saved_widget_ids[control.params.widget_id] = true;
     613                        };
     614                        wp.customize.bind( 'ready', remember_saved_widget_id );
     615                        wp.customize.bind( 'saved', remember_saved_widget_id );
     616
     617                        control._update_count = 0;
     618                        control.is_widget_updating = false;
     619
     620                        // Update widget whenever model changes
     621                        control.setting.bind( function( to, from ) {
     622                                if ( ! _( from ).isEqual( to ) && ! control.is_widget_updating ) {
     623                                        control.updateWidget( { instance: to } );
     624                                }
     625                        } );
     626                },
     627
     628                /**
     629                 * Add special behaviors for wide widget controls
     630                 */
     631                _setupWideWidget: function () {
     632                        var control = this;
     633                        if ( ! control.params.is_wide ) {
     634                                return;
     635                        }
     636                        var widget_inside = control.container.find( '.widget-inside' );
     637                        var customize_sidebar = $( '.wp-full-overlay-sidebar-content:first' );
     638                        control.container.addClass( 'wide-widget-control' );
     639
     640                        control.container.find( '.widget-content:first' ).css( {
     641                                'min-width': control.params.width,
     642                                'min-height': control.params.height
     643                        } );
     644
     645                        /**
     646                         * Keep the widget-inside positioned so the top of fixed-positioned
     647                         * element is at the same top position as the widget-top. When the
     648                         * widget-top is scrolled out of view, keep the widget-top in view;
     649                         * likewise, don't allow the widget to drop off the bottom of the window.
     650                         */
     651                        var position_widget = function () {
     652                                var offset_top = control.container.offset().top;
     653                                var height = widget_inside.outerHeight();
     654                                var top = Math.max( offset_top, 0 );
     655                                var max_top = $( window ).height() - height;
     656                                top = Math.min( top, max_top );
     657                                widget_inside.css( 'top', top );
     658                        };
     659
     660                        var theme_controls_container = $( '#customize-theme-controls' );
     661                        control.container.on( 'expand', function () {
     662                                customize_sidebar.on( 'scroll', position_widget );
     663                                $( window ).on( 'resize', position_widget );
     664                                theme_controls_container.on( 'expanded collapsed', position_widget );
     665                                position_widget();
     666                        } );
     667                        control.container.on( 'collapsed', function () {
     668                                customize_sidebar.off( 'scroll', position_widget );
     669                                theme_controls_container.off( 'expanded collapsed', position_widget );
     670                                $( window ).off( 'resize', position_widget );
     671                        } );
     672
     673                        // Reposition whenever a sidebar's widgets are changed
     674                        wp.customize.each( function ( setting ) {
     675                                if ( 0 === setting.id.indexOf( 'sidebars_widgets[' ) ) {
     676                                        setting.bind( function () {
     677                                                if ( control.container.hasClass( 'expanded' ) ) {
     678                                                        position_widget();
     679                                                }
     680                                        } );
     681                                }
     682                        } );
     683                },
     684
     685                /**
     686                 * Show/hide the control when clicking on the form title, when clicking
     687                 * the close button
     688                 */
     689                _setupControlToggle: function() {
     690                        var control = this;
     691                        control.container.find( '.widget-top' ).on( 'click', function ( e ) {
     692                                e.preventDefault();
     693                                var sidebar_widgets_control = control.getSidebarWidgetsControl();
     694                                if ( sidebar_widgets_control.is_reordering ) {
     695                                        return;
     696                                }
     697                                control.toggleForm();
     698                        } );
     699
     700                        var close_btn = control.container.find( '.widget-control-close' );
     701                        // @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
     702                        close_btn.on( 'click', function ( e ) {
     703                                e.preventDefault();
     704                                control.collapseForm();
     705                                control.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility
     706                        } );
     707                },
     708
     709                /**
     710                 * Update the title of the form if a title field is entered
     711                 */
     712                _setupWidgetTitle: function () {
     713                        var control = this;
     714                        var update_title = function () {
     715                                var title = control.setting().title;
     716                                var in_widget_title = control.container.find( '.in-widget-title' );
     717                                if ( title ) {
     718                                        in_widget_title.text( ': ' + title );
     719                                } else {
     720                                        in_widget_title.text( '' );
     721                                }
     722                        };
     723                        control.setting.bind( update_title );
     724                        update_title();
     725                },
     726
     727                /**
     728                 * Set up the widget-reorder-nav
     729                 */
     730                _setupReorderUI: function () {
     731                        var control = this;
     732
     733                        /**
     734                         * select the provided sidebar list item in the move widget area
     735                         *
     736                         * @param {jQuery} li
     737                         */
     738                        var select_sidebar_item = function ( li ) {
     739                                li.siblings( '.selected' ).removeClass( 'selected' );
     740                                li.addClass( 'selected' );
     741                                var is_self_sidebar = ( li.data( 'id' ) === control.params.sidebar_id );
     742                                control.container.find( '.move-widget-btn' ).prop( 'disabled', is_self_sidebar );
     743                        };
     744
     745                        /**
     746                         * Add the widget reordering elements to the widget control
     747                         */
     748                        control.container.find( '.widget-title-action' ).after( $( self.tpl.widget_reorder_nav ) );
     749                        var move_widget_area = $(
     750                                _.template( self.tpl.move_widget_area, {
     751                                        sidebars: _( self.registered_sidebars.toArray() ).pluck( 'attributes' )
     752                                } )
     753                        );
     754                        control.container.find( '.widget-top' ).after( move_widget_area );
     755
     756                        /**
     757                         * Update available sidebars when their rendered state changes
     758                         */
     759                        var update_available_sidebars = function () {
     760                                var sidebar_items = move_widget_area.find( 'li' );
     761                                var self_sidebar_item = sidebar_items.filter( function(){
     762                                        return $( this ).data( 'id' ) === control.params.sidebar_id;
     763                                } );
     764                                sidebar_items.each( function () {
     765                                        var li = $( this );
     766                                        var sidebar_id = li.data( 'id' );
     767                                        var sidebar_model = self.registered_sidebars.get( sidebar_id );
     768                                        li.toggle( sidebar_model.get( 'is_rendered' ) );
     769                                        if ( li.hasClass( 'selected' ) && ! sidebar_model.get( 'is_rendered' ) ) {
     770                                                select_sidebar_item( self_sidebar_item );
     771                                        }
     772                                } );
     773                        };
     774                        update_available_sidebars();
     775                        self.registered_sidebars.on( 'change:is_rendered', update_available_sidebars );
     776
     777                        /**
     778                         * Handle clicks for up/down/move on the reorder nav
     779                         */
     780                        var reorder_nav = control.container.find( '.widget-reorder-nav' );
     781                        reorder_nav.find( '.move-widget, .move-widget-down, .move-widget-up' ).on( 'click keypress', function ( event ) {
     782                                if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
     783                                        return;
     784                                }
     785                                $( this ).focus();
     786
     787                                if ( $( this ).is( '.move-widget' ) ) {
     788                                        control.toggleWidgetMoveArea();
     789                                } else {
     790                                        var is_move_down = $( this ).is( '.move-widget-down' );
     791                                        var is_move_up = $( this ).is( '.move-widget-up' );
     792                                        var i = control.getWidgetSidebarPosition();
     793                                        if ( ( is_move_up && i === 0 ) || ( is_move_down && i === control.getSidebarWidgetsControl().setting().length - 1 ) ) {
     794                                                return;
     795                                        }
     796
     797                                        if ( is_move_up ) {
     798                                                control.moveUp();
     799                                        } else {
     800                                                control.moveDown();
     801                                        }
     802
     803                                        $( this ).focus(); // re-focus after the container was moved
     804                                }
     805                        } );
     806
     807                        /**
     808                         * Handle selecting a sidebar to move to
     809                         */
     810                        control.container.find( '.widget-area-select' ).on( 'click keypress', 'li', function ( e ) {
     811                                if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
     812                                        return;
     813                                }
     814                                e.preventDefault();
     815                                select_sidebar_item( $( this ) );
     816                        } );
     817
     818                        /**
     819                         * Move widget to another sidebar
     820                         */
     821                        control.container.find( '.move-widget-btn' ).click( function () {
     822                                control.getSidebarWidgetsControl().toggleReordering( false );
     823
     824                                var old_sidebar_id = control.params.sidebar_id;
     825                                var new_sidebar_id = control.container.find( '.widget-area-select li.selected' ).data( 'id' );
     826                                var old_sidebar_widgets_setting = customize( 'sidebars_widgets[' + old_sidebar_id + ']' );
     827                                var new_sidebar_widgets_setting = customize( 'sidebars_widgets[' + new_sidebar_id + ']' );
     828                                var old_sidebar_widget_ids = Array.prototype.slice.call( old_sidebar_widgets_setting() );
     829                                var new_sidebar_widget_ids = Array.prototype.slice.call( new_sidebar_widgets_setting() );
     830
     831                                var i = control.getWidgetSidebarPosition();
     832                                old_sidebar_widget_ids.splice( i, 1 );
     833                                new_sidebar_widget_ids.push( control.params.widget_id );
     834
     835                                old_sidebar_widgets_setting( old_sidebar_widget_ids );
     836                                new_sidebar_widgets_setting( new_sidebar_widget_ids );
     837
     838                                control.focus();
     839                        } );
     840                },
     841
     842                /**
     843                 * Highlight widgets in preview when interacted with in the customizer
     844                 */
     845                _setupHighlightEffects: function() {
     846                        var control = this;
     847
     848                        // Highlight whenever hovering or clicking over the form
     849                        control.container.on( 'mouseenter click', function () {
     850                                control.highlightPreviewWidget();
     851                        } );
     852
     853                        // Highlight when the setting is updated
     854                        control.setting.bind( function () {
     855                                control.scrollPreviewWidgetIntoView();
     856                                control.highlightPreviewWidget();
     857                        } );
     858
     859                        // Highlight when the widget form is expanded
     860                        control.container.on( 'expand', function () {
     861                                control.scrollPreviewWidgetIntoView();
     862                        } );
     863                },
     864
     865                /**
     866                 * Set up event handlers for widget updating
     867                 */
     868                _setupUpdateUI: function () {
     869                        var control = this;
     870
     871                        control.container.toggleClass( 'is-live-previewable', control.params.is_live_previewable );
     872                        var widget_content = control.container.find( '.widget-content' );
     873
     874                        // Configure update button
     875                        var save_btn = control.container.find( '.widget-control-save' );
     876                        save_btn.val( self.i18n.save_btn_label );
     877                        save_btn.attr( 'title', self.i18n.save_btn_tooltip );
     878                        save_btn.removeClass( 'button-primary' ).addClass( 'button-secondary' );
     879                        save_btn.on( 'click', function ( e ) {
     880                                e.preventDefault();
     881                                control.updateWidget();
     882                        } );
     883
     884                        var trigger_save = _.debounce( function () {
     885                                // @todo For compatibility with other plugins, should we trigger a click event? What about form submit event?
     886                                control.updateWidget();
     887                        }, 250 );
     888
     889                        // Trigger widget form update when hitting Enter within an input
     890                        control.container.find( '.widget-content' ).on( 'keydown', 'input', function( e ) {
     891                                if ( 13 === e.which ) { // Enter
     892                                        e.preventDefault();
     893                                        control.updateWidget( { ignore_active_element: true } );
     894                                }
     895                        } );
     896
     897                        // Handle widgets that support live previews
     898                        if ( control.params.is_live_previewable ) {
     899                                widget_content.on( 'change input propertychange', ':input', function ( e ) {
     900                                        if ( e.type === 'change' || ( this.checkValidity && this.checkValidity() ) ) {
     901                                                trigger_save();
     902                                        }
     903                                } );
     904                        }
     905
     906                        // Remove loading indicators when the setting is saved and the preview updates
     907                        control.setting.previewer.channel.bind( 'synced', function () {
     908                                control.container.removeClass( 'previewer-loading' );
     909                        } );
     910                        self.previewer.bind( 'widget-updated', function ( updated_widget_id ) {
     911                                if ( updated_widget_id === control.params.widget_id ) {
     912                                        control.container.removeClass( 'previewer-loading' );
     913                                }
     914                        } );
     915
     916                        // Update widget control to indicate whether it is currently rendered (cf. Widget Visibility)
     917                        self.previewer.bind( 'rendered-widgets', function ( rendered_widgets ) {
     918                                var is_rendered = !! rendered_widgets[control.params.widget_id];
     919                                control.container.toggleClass( 'widget-rendered', is_rendered );
     920                        } );
     921                },
     922
     923                /**
     924                 * Set up event handlers for widget removal
     925                 */
     926                _setupRemoveUI: function () {
     927                        var control = this;
     928
     929                        // Configure remove button
     930                        var remove_btn = control.container.find( 'a.widget-control-remove' );
     931                        // @todo Hitting Enter on this link does nothing; will be resolved in core with <http://core.trac.wordpress.org/ticket/26633>
     932                        remove_btn.on( 'click', function ( e ) {
     933                                e.preventDefault();
     934
     935                                // Find an adjacent element to add focus to when this widget goes away
     936                                var adjacent_focus_target;
     937                                if ( control.container.next().is( '.customize-control-widget_form' ) ) {
     938                                        adjacent_focus_target = control.container.next().find( '.widget-action:first' );
     939                                } else if ( control.container.prev().is( '.customize-control-widget_form' ) ) {
     940                                        adjacent_focus_target = control.container.prev().find( '.widget-action:first' );
     941                                } else {
     942                                        adjacent_focus_target = control.container.next( '.customize-control-sidebar_widgets' ).find( '.add-new-widget:first' );
     943                                }
     944
     945                                control.container.slideUp( function() {
     946                                        var sidebars_widgets_control = self.getSidebarWidgetControlContainingWidget( control.params.widget_id );
     947                                        if ( ! sidebars_widgets_control ) {
     948                                                throw new Error( 'Unable to find sidebars_widgets_control' );
     949                                        }
     950                                        var sidebar_widget_ids = sidebars_widgets_control.setting().slice();
     951                                        var i = sidebar_widget_ids.indexOf( control.params.widget_id );
     952                                        if ( -1 === i ) {
     953                                                throw new Error( 'Widget is not in sidebar' );
     954                                        }
     955                                        sidebar_widget_ids.splice( i, 1 );
     956                                        sidebars_widgets_control.setting( sidebar_widget_ids );
     957                                        adjacent_focus_target.focus(); // keyboard accessibility
     958                                } );
     959                        } );
     960
     961                        var replace_delete_with_remove = function () {
     962                                remove_btn.text( self.i18n.remove_btn_label ); // wp_widget_control() outputs the link as "Delete"
     963                                remove_btn.attr( 'title', self.i18n.remove_btn_tooltip );
     964                        };
     965                        if ( control.params.is_new ) {
     966                                wp.customize.bind( 'saved', replace_delete_with_remove );
     967                        } else {
     968                                replace_delete_with_remove();
     969                        }
     970                },
     971
     972                /**
     973                 * Iterate over supplied inputs and create a signature string for all of them together.
     974                 * This string can be used to compare whether or not the form has all of the same fields.
     975                 *
     976                 * @param {jQuery} inputs
     977                 * @returns {string}
     978                 * @private
     979                 */
     980                _getInputsSignature: function ( inputs ) {
     981                        var inputs_signatures = _( inputs ).map( function ( input ) {
     982                                input = $( input );
     983                                var signature_parts;
     984                                if ( input.is( 'option' ) ) {
     985                                        signature_parts = [ input.prop( 'nodeName' ), input.prop( 'value' ) ];
     986                                } else if ( input.is( ':checkbox, :radio' ) ) {
     987                                        signature_parts = [ input.prop( 'type' ), input.attr( 'id' ), input.attr( 'name' ), input.prop( 'value' ) ];
     988                                } else {
     989                                        signature_parts = [ input.prop( 'nodeName' ), input.attr( 'id' ), input.attr( 'name' ), input.attr( 'type' ) ];
     990                                }
     991                                return signature_parts.join( ',' );
     992                        } );
     993                        return inputs_signatures.join( ';' );
     994                },
     995
     996                /**
     997                 * Get the property that represents the state of an input.
     998                 *
     999                 * @param {jQuery|DOMElement} input
     1000                 * @returns {string}
     1001                 * @private
     1002                 */
     1003                _getInputStatePropertyName: function ( input ) {
     1004                        input = $( input );
     1005                        if ( input.is( ':radio, :checkbox' ) ) {
     1006                                return 'checked';
     1007                        } else if ( input.is( 'option' ) ) {
     1008                                return 'selected';
     1009                        } else {
     1010                                return 'value';
     1011                        }
     1012                },
     1013
     1014                /***********************************************************************
     1015                 * Begin public API methods
     1016                 **********************************************************************/
     1017
     1018                /**
     1019                 * @return {wp.customize.controlConstructor.sidebar_widgets[]}
     1020                 */
     1021                getSidebarWidgetsControl: function () {
     1022                        var control = this;
     1023                        var setting_id = 'sidebars_widgets[' + control.params.sidebar_id + ']';
     1024                        var sidebar_widgets_control = customize.control( setting_id );
     1025                        if ( ! sidebar_widgets_control ) {
     1026                                throw new Error( 'Unable to locate sidebar_widgets control for ' + control.params.sidebar_id );
     1027                        }
     1028                        return sidebar_widgets_control;
     1029                },
     1030
     1031                /**
     1032                 * Submit the widget form via Ajax and get back the updated instance,
     1033                 * along with the new widget control form to render.
     1034                 *
     1035                 * @param {object} [args]
     1036                 * @param {Object|null} [args.instance=null]  When the model changes, the instance is sent here; otherwise, the inputs from the form are used
     1037                 * @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.
     1038                 * @param {Boolean} [args.ignore_active_element=false] Whether or not updating a field will be deferred if focus is still on the element.
     1039                 */
     1040                updateWidget: function ( args ) {
     1041                        var control = this;
     1042                        args = $.extend( {
     1043                                instance: null,
     1044                                complete: null,
     1045                                ignore_active_element: false
     1046                        }, args );
     1047                        var instance_override = args.instance;
     1048                        var complete_callback = args.complete;
     1049
     1050                        control._update_count += 1;
     1051                        var update_number = control._update_count;
     1052
     1053                        var widget_content = control.container.find( '.widget-content' );
     1054
     1055                        var element_id_to_refocus = null;
     1056                        var active_input_selection_start = null;
     1057                        var active_input_selection_end = null;
     1058                        // @todo Support more selectors than IDs?
     1059                        if ( $.contains( control.container[0], document.activeElement ) && $( document.activeElement ).is( '[id]' ) ) {
     1060                                element_id_to_refocus = $( document.activeElement ).prop( 'id' );
     1061                                // @todo IE8 support: http://stackoverflow.com/a/4207763/93579
     1062                                try {
     1063                                        active_input_selection_start = document.activeElement.selectionStart;
     1064                                        active_input_selection_end = document.activeElement.selectionEnd;
     1065                                }
     1066                                catch( e ) {} // catch InvalidStateError in case of checkboxes
     1067                        }
     1068
     1069                        control.container.addClass( 'widget-form-loading' );
     1070                        control.container.addClass( 'previewer-loading' );
     1071
     1072                        if ( ! control.params.is_live_previewable ) {
     1073                                widget_content.prop( 'disabled', true );
     1074                        }
     1075
     1076                        var params = {};
     1077                        params.action = self.update_widget_ajax_action;
     1078                        params[self.update_widget_nonce_post_key] = self.update_widget_nonce_value;
     1079
     1080                        var data = $.param( params );
     1081                        var inputs = widget_content.find( ':input, option' );
     1082
     1083                        // Store the value we're submitting in data so that when the response comes back,
     1084                        // we know if it got sanitized; if there is no difference in the sanitized value,
     1085                        // then we do not need to touch the UI and mess up the user's ongoing editing.
     1086                        inputs.each( function () {
     1087                                var input = $( this );
     1088                                var property = control._getInputStatePropertyName( this );
     1089                                input.data( 'state' + update_number, input.prop( property ) );
     1090                        } );
     1091
     1092                        if ( instance_override ) {
     1093                                data += '&' + $.param( { 'sanitized_widget_setting': JSON.stringify( instance_override ) } );
     1094                        } else {
     1095                                data += '&' + inputs.serialize();
     1096                        }
     1097                        data += '&' + widget_content.find( '~ :input' ).serialize();
     1098
     1099                        var jqxhr = $.post( wp.ajax.settings.url, data, function ( r ) {
     1100                                if ( r.success ) {
     1101                                        var sanitized_form = $( '<div>' + r.data.form + '</div>' );
     1102                                        control.hook( 'formUpdate', sanitized_form );
     1103
     1104                                        var sanitized_inputs = sanitized_form.find( ':input, option' );
     1105                                        var has_same_inputs_in_response = control._getInputsSignature( inputs ) === control._getInputsSignature( sanitized_inputs );
     1106
     1107                                        if ( control.params.is_live_previewable && has_same_inputs_in_response ) {
     1108                                                inputs.each( function ( i ) {
     1109                                                        var input = $( this );
     1110                                                        var sanitized_input = $( sanitized_inputs[i] );
     1111                                                        var property = control._getInputStatePropertyName( this );
     1112                                                        var state = input.data( 'state' + update_number );
     1113                                                        var sanitized_state = sanitized_input.prop( property );
     1114                                                        input.data( 'sanitized', sanitized_state );
     1115
     1116                                                        if ( state !== sanitized_state ) {
     1117
     1118                                                                // Only update now if not currently focused on it,
     1119                                                                // so that we don't cause the cursor
     1120                                                                // it will be updated upon the change event
     1121                                                                if ( args.ignore_active_element || ! input.is( document.activeElement ) ) {
     1122                                                                        input.prop( property, sanitized_state );
     1123                                                                }
     1124                                                                control.hook( 'unsanitaryField', input, sanitized_state, state );
     1125
     1126                                                        } else {
     1127                                                                control.hook( 'sanitaryField', input, state );
     1128                                                        }
     1129                                                } );
     1130                                                control.hook( 'formUpdated', sanitized_form );
     1131                                        } else {
     1132                                                widget_content.html( sanitized_form.html() );
     1133                                                if ( element_id_to_refocus ) {
     1134                                                        // not using jQuery selector so we don't have to worry about escaping IDs with brackets and other characters
     1135                                                        $( document.getElementById( element_id_to_refocus ) )
     1136                                                                .prop( {
     1137                                                                        selectionStart: active_input_selection_start,
     1138                                                                        selectionEnd: active_input_selection_end
     1139                                                                } )
     1140                                                                .focus();
     1141                                                }
     1142                                                control.hook( 'formRefreshed' );
     1143                                        }
     1144
     1145                                        /**
     1146                                         * If the old instance is identical to the new one, there is nothing new
     1147                                         * needing to be rendered, and so we can preempt the event for the
     1148                                         * preview finishing loading.
     1149                                         */
     1150                                        var is_instance_identical = _( control.setting() ).isEqual( r.data.instance );
     1151                                        if ( is_instance_identical ) {
     1152                                                control.container.removeClass( 'previewer-loading' );
     1153                                        } else {
     1154                                                control.is_widget_updating = true; // suppress triggering another updateWidget
     1155                                                control.setting( r.data.instance );
     1156                                                control.is_widget_updating = false;
     1157                                        }
     1158
     1159                                        if ( complete_callback ) {
     1160                                                complete_callback.call( control, null, { no_change: is_instance_identical, ajax_finished: true } );
     1161                                        }
     1162                                } else {
     1163                                        var message = 'FAIL';
     1164                                        if ( r.data && r.data.message ) {
     1165                                                message = r.data.message;
     1166                                        }
     1167                                        if ( complete_callback ) {
     1168                                                complete_callback.call( control, message );
     1169                                        } else {
     1170                                                throw new Error( message );
     1171                                        }
     1172                                }
     1173                        } );
     1174                        jqxhr.fail( function ( jqXHR, textStatus ) {
     1175                                if ( complete_callback ) {
     1176                                        complete_callback.call( control, textStatus );
     1177                                } else {
     1178                                        throw new Error( textStatus );
     1179                                }
     1180                        } );
     1181                        jqxhr.always( function () {
     1182                                if ( ! control.params.is_live_previewable ) {
     1183                                        widget_content.prop( 'disabled', false );
     1184                                        control.container.removeClass( 'widget-form-loading' );
     1185                                }
     1186
     1187                                inputs.each( function () {
     1188                                        $( this ).removeData( 'state' + update_number );
     1189                                } );
     1190                        } );
     1191                },
     1192
     1193                /**
     1194                 * Expand the accordion section containing a control
     1195                 * @todo it would be nice if accordion had a proper API instead of having to trigger UI events on its elements
     1196                 */
     1197                expandControlSection: function () {
     1198                        var section = this.container.closest( '.accordion-section' );
     1199                        if ( ! section.hasClass( 'open' ) ) {
     1200                                section.find( '.accordion-section-title:first' ).trigger( 'click' );
     1201                        }
     1202                },
     1203
     1204                /**
     1205                 * Expand the widget form control
     1206                 */
     1207                expandForm: function () {
     1208                        this.toggleForm( true );
     1209                },
     1210
     1211                /**
     1212                 * Collapse the widget form control
     1213                 */
     1214                collapseForm: function () {
     1215                        this.toggleForm( false );
     1216                },
     1217
     1218                /**
     1219                 * Expand or collapse the widget control
     1220                 *
     1221                 * @param {boolean|undefined} [do_expand] If not supplied, will be inverse of current visibility
     1222                 */
     1223                toggleForm: function ( do_expand ) {
     1224                        var control = this;
     1225                        var widget = control.container.find( 'div.widget:first' );
     1226                        var inside = widget.find( '.widget-inside:first' );
     1227                        if ( typeof do_expand === 'undefined' ) {
     1228                                do_expand = ! inside.is( ':visible' );
     1229                        }
     1230
     1231                        // Already expanded or collapsed, so noop
     1232                        if ( inside.is( ':visible' ) === do_expand ) {
     1233                                return;
     1234                        }
     1235
     1236                        var complete;
     1237                        if ( do_expand ) {
     1238                                // Close all other widget controls before expanding this one
     1239                                wp.customize.control.each( function ( other_control ) {
     1240                                        if ( control.params.type === other_control.params.type && control !== other_control ) {
     1241                                                other_control.collapseForm();
     1242                                        }
     1243                                } );
     1244
     1245                                control.container.trigger( 'expand' );
     1246                                control.container.addClass( 'expanding' );
     1247                                complete = function () {
     1248                                        control.container.removeClass( 'expanding' );
     1249                                        control.container.addClass( 'expanded' );
     1250                                        control.container.trigger( 'expanded' );
     1251                                };
     1252                                if ( control.params.is_wide ) {
     1253                                        inside.animate( { width: 'show' }, 'fast', complete );
     1254                                } else {
     1255                                        inside.slideDown( 'fast', complete );
     1256                                }
     1257                        } else {
     1258                                control.container.trigger( 'collapse' );
     1259                                control.container.addClass( 'collapsing' );
     1260                                complete = function () {
     1261                                        control.container.removeClass( 'collapsing' );
     1262                                        control.container.removeClass( 'expanded' );
     1263                                        control.container.trigger( 'collapsed' );
     1264                                };
     1265                                if ( control.params.is_wide ) {
     1266                                        inside.animate( { width: 'hide' }, 'fast', complete );
     1267                                } else {
     1268                                        inside.slideUp( 'fast', function() {
     1269                                                widget.css( { width:'', margin:'' } );
     1270                                                complete();
     1271                                        } );
     1272                                }
     1273                        }
     1274                },
     1275
     1276                /**
     1277                 * Expand the containing sidebar section, expand the form, and focus on
     1278                 * the first input in the control
     1279                 */
     1280                focus: function () {
     1281                        var control = this;
     1282                        control.expandControlSection();
     1283                        control.expandForm();
     1284                        control.container.find( ':focusable:first' ).focus().trigger( 'click' );
     1285                },
     1286
     1287                /**
     1288                 * Get the position (index) of the widget in the containing sidebar
     1289                 *
     1290                 * @throws Error
     1291                 * @returns {Number}
     1292                 */
     1293                getWidgetSidebarPosition: function () {
     1294                        var control = this;
     1295                        var sidebar_widget_ids = control.getSidebarWidgetsControl().setting();
     1296                        var position = sidebar_widget_ids.indexOf( control.params.widget_id );
     1297                        if ( position === -1 ) {
     1298                                throw new Error( 'Widget was unexpectedly not present in the sidebar.' );
     1299                        }
     1300                        return position;
     1301                },
     1302
     1303                /**
     1304                 * Move widget up one in the sidebar
     1305                 */
     1306                moveUp: function () {
     1307                        this._moveWidgetByOne( -1 );
     1308                },
     1309
     1310                /**
     1311                 * Move widget up one in the sidebar
     1312                 */
     1313                moveDown: function () {
     1314                        this._moveWidgetByOne( 1 );
     1315                },
     1316
     1317                /**
     1318                 * @private
     1319                 *
     1320                 * @param {Number} offset 1|-1
     1321                 */
     1322                _moveWidgetByOne: function ( offset ) {
     1323                        var control = this;
     1324                        var i = control.getWidgetSidebarPosition();
     1325
     1326                        var sidebar_widgets_setting = control.getSidebarWidgetsControl().setting;
     1327                        var sidebar_widget_ids = Array.prototype.slice.call( sidebar_widgets_setting() ); // clone
     1328                        var adjacent_widget_id = sidebar_widget_ids[i + offset];
     1329                        sidebar_widget_ids[i + offset] = control.params.widget_id;
     1330                        sidebar_widget_ids[i] = adjacent_widget_id;
     1331
     1332                        sidebar_widgets_setting( sidebar_widget_ids );
     1333                },
     1334
     1335                /**
     1336                 * Toggle visibility of the widget move area
     1337                 *
     1338                 * @param {Boolean} [toggle]
     1339                 */
     1340                toggleWidgetMoveArea: function ( toggle ) {
     1341                        var control = this;
     1342                        var move_widget_area = control.container.find( '.move-widget-area' );
     1343                        if ( typeof toggle === 'undefined' ) {
     1344                                toggle = ! move_widget_area.hasClass( 'active' );
     1345                        }
     1346                        if ( toggle ) {
     1347                                // reset the selected sidebar
     1348                                move_widget_area.find( '.selected' ).removeClass( 'selected' );
     1349                                move_widget_area.find( 'li' ).filter( function () {
     1350                                        return $( this ).data( 'id' ) === control.params.sidebar_id;
     1351                                } ).addClass( 'selected' );
     1352                                control.container.find( '.move-widget-btn' ).prop( 'disabled', true );
     1353                        }
     1354                        move_widget_area.toggleClass( 'active', toggle );
     1355                },
     1356
     1357                /**
     1358                 * Inverse of WidgetCustomizer.getControlInstanceForWidget
     1359                 * @return {jQuery}
     1360                 */
     1361                getPreviewWidgetElement: function () {
     1362                        var control = this;
     1363                        var widget_customizer_preview = self.getPreviewWindow().WidgetCustomizerPreview;
     1364                        return widget_customizer_preview.getSidebarWidgetElement( control.params.sidebar_id, control.params.widget_id );
     1365                },
     1366
     1367                /**
     1368                 * Inside of the customizer preview, scroll the widget into view
     1369                 */
     1370                scrollPreviewWidgetIntoView: function () {
     1371                        // @todo scrollIntoView() provides a robust but very poor experience. Animation is needed. See https://github.com/x-team/wp-widget-customizer/issues/16
     1372                },
     1373
     1374                /**
     1375                 * Highlight the widget control and section
     1376                 */
     1377                highlightSectionAndControl: function() {
     1378                        var control = this;
     1379                        var target_element;
     1380                        if ( control.container.is( ':hidden' ) ) {
     1381                                target_element = control.container.closest( '.control-section' );
     1382                        } else {
     1383                                target_element = control.container;
     1384                        }
     1385
     1386                        $( '.widget-customizer-highlighted' ).removeClass( 'widget-customizer-highlighted' );
     1387                        target_element.addClass( 'widget-customizer-highlighted' );
     1388                        setTimeout( function () {
     1389                                target_element.removeClass( 'widget-customizer-highlighted' );
     1390                        }, 500 );
     1391                },
     1392
     1393                /**
     1394                 * Add the widget-customizer-highlighted-widget class to the widget for 500ms
     1395                 */
     1396                highlightPreviewWidget: function () {
     1397                        var control = this;
     1398                        var widget_el = control.getPreviewWidgetElement();
     1399                        var root_el = widget_el.closest( 'html' );
     1400                        root_el.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );
     1401                        widget_el.addClass( 'widget-customizer-highlighted-widget' );
     1402                        setTimeout( function () {
     1403                                widget_el.removeClass( 'widget-customizer-highlighted-widget' );
     1404                        }, 500 );
     1405                }
     1406
     1407        } );
     1408
     1409        /**
     1410         * Capture the instance of the Previewer since it is private
     1411         */
     1412        var OldPreviewer = wp.customize.Previewer;
     1413        wp.customize.Previewer = OldPreviewer.extend( {
     1414                initialize: function( params, options ) {
     1415                        self.previewer = this;
     1416                        OldPreviewer.prototype.initialize.call( this, params, options );
     1417                        this.bind( 'refresh', this.refresh );
     1418                }
     1419        } );
     1420
     1421        /**
     1422         * Given a widget control, find the sidebar widgets control that contains it.
     1423         * @param {string} widget_id
     1424         * @return {object|null}
     1425         */
     1426        self.getSidebarWidgetControlContainingWidget = function ( widget_id ) {
     1427                var found_control = null;
     1428                // @todo this can use widget_id_to_setting_id(), then pass into wp.customize.control( x ).getSidebarWidgetsControl()
     1429                wp.customize.control.each( function ( control ) {
     1430                        if ( control.params.type === 'sidebar_widgets' && -1 !== control.setting().indexOf( widget_id ) ) {
     1431                                found_control = control;
     1432                        }
     1433                } );
     1434                return found_control;
     1435        };
     1436
     1437        /**
     1438         * Given a widget_id for a widget appearing in the preview, get the widget form control associated with it
     1439         * @param {string} widget_id
     1440         * @return {object|null}
     1441         */
     1442        self.getWidgetFormControlForWidget = function ( widget_id ) {
     1443                var found_control = null;
     1444                // @todo We can just use widget_id_to_setting_id() here
     1445                wp.customize.control.each( function ( control ) {
     1446                        if ( control.params.type === 'widget_form' && control.params.widget_id === widget_id ) {
     1447                                found_control = control;
     1448                        }
     1449                } );
     1450                return found_control;
     1451        };
     1452
     1453        /**
     1454         * @returns {Window}
     1455         */
     1456        self.getPreviewWindow = function (){
     1457                return $( '#customize-preview' ).find( 'iframe' ).prop( 'contentWindow' );
     1458        };
     1459
     1460        /**
     1461         * Available Widgets Panel
     1462         */
     1463        self.availableWidgetsPanel = {
     1464                active_sidebar_widgets_control: null,
     1465                selected_widget_tpl: null,
     1466                container: null,
     1467                filter_input: null,
     1468
     1469                /**
     1470                 * Set up event listeners
     1471                 */
     1472                setup: function () {
     1473                        var panel = this;
     1474                        panel.container = $( '#available-widgets' );
     1475                        panel.filter_input = $( '#available-widgets-filter' ).find( 'input' );
     1476
     1477                        var update_available_widgets_list = function () {
     1478                                self.available_widgets.each( function ( widget ) {
     1479                                        var widget_tpl = $( '#widget-tpl-' + widget.id );
     1480                                        widget_tpl.toggle( ! widget.get( 'is_disabled' ) );
     1481                                        if ( widget.get( 'is_disabled' ) && widget_tpl.is( panel.selected_widget_tpl ) ) {
     1482                                                panel.selected_widget_tpl = null;
     1483                                        }
     1484                                } );
     1485                        };
     1486
     1487                        self.available_widgets.on( 'change', update_available_widgets_list );
     1488                        update_available_widgets_list();
     1489
     1490                        // If the available widgets panel is open and the customize controls are
     1491                        // interacted with (i.e. available widgets panel is blurred) then close the
     1492                        // available widgets panel.
     1493                        $( '#customize-controls' ).on( 'click keydown', function ( e ) {
     1494                                var is_add_new_widget_btn = $( e.target ).is( '.add-new-widget, .add-new-widget *' );
     1495                                if ( $( 'body' ).hasClass( 'adding-widget' ) && ! is_add_new_widget_btn ) {
     1496                                        panel.close();
     1497                                }
     1498                        } );
     1499
     1500                        // Close the panel if the URL in the preview changes
     1501                        self.previewer.bind( 'url', function () {
     1502                                panel.close();
     1503                        } );
     1504
     1505                        // Submit a selection when clicked or keypressed
     1506                        panel.container.find( '.widget-tpl' ).on( 'click keypress', function( event ) {
     1507
     1508                                // Only proceed with keypress if it is Enter or Spacebar
     1509                                if ( event.type === 'keypress' && ( event.which !== 13 && event.which !== 32 ) ) {
     1510                                        return;
     1511                                }
     1512
     1513                                panel.submit( this );
     1514                        } );
     1515
     1516                        panel.container.liveFilter(
     1517                                '#available-widgets-filter input',
     1518                                '.widget-tpl',
     1519                                {
     1520                                        filterChildSelector: '.widget-title h4',
     1521                                        after: function () {
     1522                                                var filter_val = panel.filter_input.val();
     1523
     1524                                                // Remove a widget from being selected if it is no longer visible
     1525                                                if ( panel.selected_widget_tpl && ! panel.selected_widget_tpl.is( ':visible' ) ) {
     1526                                                        panel.selected_widget_tpl.removeClass( 'selected' );
     1527                                                        panel.selected_widget_tpl = null;
     1528                                                }
     1529
     1530                                                // If a widget was selected but the filter value has been cleared out, clear selection
     1531                                                if ( panel.selected_widget_tpl && ! filter_val ) {
     1532                                                        panel.selected_widget_tpl.removeClass( 'selected' );
     1533                                                        panel.selected_widget_tpl = null;
     1534                                                }
     1535
     1536                                                // If a filter has been entered and a widget hasn't been selected, select the first one shown
     1537                                                if ( ! panel.selected_widget_tpl && filter_val ) {
     1538                                                        var first_visible_widget = panel.container.find( '> .widget-tpl:visible:first' );
     1539                                                        if ( first_visible_widget.length ) {
     1540                                                                panel.select( first_visible_widget );
     1541                                                        }
     1542                                                }
     1543
     1544                                        }
     1545                                }
     1546                        );
     1547
     1548                        // Select a widget when it is focused on
     1549                        panel.container.find( ' > .widget-tpl' ).on( 'focus', function () {
     1550                                panel.select( this );
     1551                        } );
     1552
     1553                        panel.container.on( 'keydown', function ( event ) {
     1554                                var is_enter = ( event.which === 13 );
     1555                                var is_esc = ( event.which === 27 );
     1556                                var is_down = ( event.which === 40 );
     1557                                var is_up = ( event.which === 38 );
     1558                                var selected_widget_tpl = null;
     1559                                var first_visible_widget = panel.container.find( '> .widget-tpl:visible:first' );
     1560                                var last_visible_widget = panel.container.find( '> .widget-tpl:visible:last' );
     1561                                var is_input_focused = $( event.target ).is( panel.filter_input );
     1562
     1563                                if ( is_down || is_up ) {
     1564                                        if ( is_down ) {
     1565                                                if ( is_input_focused ) {
     1566                                                        selected_widget_tpl = first_visible_widget;
     1567                                                } else if ( panel.selected_widget_tpl && panel.selected_widget_tpl.nextAll( '.widget-tpl:visible' ).length !== 0 ) {
     1568                                                        selected_widget_tpl = panel.selected_widget_tpl.nextAll( '.widget-tpl:visible:first' );
     1569                                                }
     1570                                        } else if ( is_up ) {
     1571                                                if ( is_input_focused ) {
     1572                                                        selected_widget_tpl = last_visible_widget;
     1573                                                } else if ( panel.selected_widget_tpl && panel.selected_widget_tpl.prevAll( '.widget-tpl:visible' ).length !== 0 ) {
     1574                                                        selected_widget_tpl = panel.selected_widget_tpl.prevAll( '.widget-tpl:visible:first' );
     1575                                                }
     1576                                        }
     1577                                        panel.select( selected_widget_tpl );
     1578                                        if ( selected_widget_tpl ) {
     1579                                                selected_widget_tpl.focus();
     1580                                        } else {
     1581                                                panel.filter_input.focus();
     1582                                        }
     1583                                        return;
     1584                                }
     1585
     1586                                // If enter pressed but nothing entered, don't do anything
     1587                                if ( is_enter && ! panel.filter_input.val() ) {
     1588                                        return;
     1589                                }
     1590
     1591                                if ( is_enter ) {
     1592                                        panel.submit();
     1593                                } else if ( is_esc ) {
     1594                                        panel.close( { return_focus: true } );
     1595                                }
     1596                        } );
     1597                },
     1598
     1599                /**
     1600                 * @param widget_tpl
     1601                 */
     1602                select: function ( widget_tpl ) {
     1603                        var panel = this;
     1604                        panel.selected_widget_tpl = $( widget_tpl );
     1605                        panel.selected_widget_tpl.siblings( '.widget-tpl' ).removeClass( 'selected' );
     1606                        panel.selected_widget_tpl.addClass( 'selected' );
     1607                },
     1608
     1609                submit: function ( widget_tpl ) {
     1610                        var panel = this;
     1611                        if ( ! widget_tpl ) {
     1612                                widget_tpl = panel.selected_widget_tpl;
     1613                        }
     1614                        if ( ! widget_tpl || ! panel.active_sidebar_widgets_control ) {
     1615                                return;
     1616                        }
     1617                        panel.select( widget_tpl );
     1618
     1619                        var widget_id = $( panel.selected_widget_tpl ).data( 'widget-id' );
     1620                        var widget = self.available_widgets.findWhere( {id: widget_id} );
     1621                        if ( ! widget ) {
     1622                                throw new Error( 'Widget unexpectedly not found.' );
     1623                        }
     1624                        panel.active_sidebar_widgets_control.addWidget( widget.get( 'id_base' ) );
     1625                        panel.close();
     1626                },
     1627
     1628                /**
     1629                 * @param sidebars_widgets_control
     1630                 */
     1631                open: function ( sidebars_widgets_control ) {
     1632                        var panel = this;
     1633                        panel.active_sidebar_widgets_control = sidebars_widgets_control;
     1634
     1635                        // Wide widget controls appear over the preview, and so they need to be collapsed when the panel opens
     1636                        _( sidebars_widgets_control.getWidgetFormControls() ).each( function ( control ) {
     1637                                if ( control.params.is_wide ) {
     1638                                        control.collapseForm();
     1639                                }
     1640                        } );
     1641
     1642                        $( 'body' ).addClass( 'adding-widget' );
     1643                        panel.container.find( '.widget-tpl' ).removeClass( 'selected' );
     1644                        panel.filter_input.focus();
     1645                },
     1646
     1647                /**
     1648                 * Hide the panel
     1649                 */
     1650                close: function ( options ) {
     1651                        var panel = this;
     1652                        options = options || {};
     1653                        if ( options.return_focus && panel.active_sidebar_widgets_control ) {
     1654                                panel.active_sidebar_widgets_control.container.find( '.add-new-widget' ).focus();
     1655                        }
     1656                        panel.active_sidebar_widgets_control = null;
     1657                        panel.selected_widget_tpl = null;
     1658                        $( 'body' ).removeClass( 'adding-widget' );
     1659                        panel.filter_input.val( '' );
     1660                }
     1661        };
     1662
     1663        /**
     1664         * @param {String} widget_id
     1665         * @returns {Object}
     1666         */
     1667        function parse_widget_id( widget_id ) {
     1668                var parsed = {
     1669                        number: null,
     1670                        id_base: null
     1671                };
     1672                var matches = widget_id.match( /^(.+)-(\d+)$/ );
     1673                if ( matches ) {
     1674                        parsed.id_base = matches[1];
     1675                        parsed.number = parseInt( matches[2], 10 );
     1676                } else {
     1677                        // likely an old single widget
     1678                        parsed.id_base = widget_id;
     1679                }
     1680                return parsed;
     1681        }
     1682
     1683        /**
     1684         * @param {String} widget_id
     1685         * @returns {String} setting_id
     1686         */
     1687        function widget_id_to_setting_id( widget_id ) {
     1688                var parsed = parse_widget_id( widget_id );
     1689                var setting_id = 'widget_' + parsed.id_base;
     1690                if ( parsed.number ) {
     1691                        setting_id += '[' + parsed.number + ']';
     1692                }
     1693                return setting_id;
     1694        }
     1695
     1696        return self;
     1697}( jQuery ));
     1698
     1699/* @todo remove this dependency */
     1700/*
     1701 * jQuery.liveFilter
     1702 *
     1703 * Copyright (c) 2009 Mike Merritt
     1704 *
     1705 * Forked by Lim Chee Aun (cheeaun.com)
     1706 *
     1707 */
     1708
     1709(function($){
     1710        $.fn.liveFilter = function(inputEl, filterEl, options){
     1711                var defaults = {
     1712                        filterChildSelector: null,
     1713                        filter: function(el, val){
     1714                                return $(el).text().toUpperCase().indexOf(val.toUpperCase()) >= 0;
     1715                        },
     1716                        before: function(){},
     1717                        after: function(){}
     1718                };
     1719                options = $.extend(defaults, options);
     1720
     1721                var el = $(this).find(filterEl);
     1722                if (options.filterChildSelector) {
     1723                        el = el.find(options.filterChildSelector);
     1724                }
     1725
     1726                var filter = options.filter;
     1727                $(inputEl).keyup(function(){
     1728                        var val = $(this).val();
     1729                        var contains = el.filter(function(){
     1730                                return filter(this, val);
     1731                        });
     1732                        var containsNot = el.not(contains);
     1733                        if (options.filterChildSelector){
     1734                                contains = contains.parents(filterEl);
     1735                                containsNot = containsNot.parents(filterEl).hide();
     1736                        }
     1737
     1738                        options.before.call(this, contains, containsNot);
     1739
     1740                        contains.show();
     1741                        containsNot.hide();
     1742
     1743                        if (val === '') {
     1744                                contains.show();
     1745                                containsNot.show();
     1746                        }
     1747
     1748                        options.after.call(this, contains, containsNot);
     1749                });
     1750        };
     1751})(jQuery);
  • src/wp-content/themes/twentyfourteen/functions.php

     
    113113
    114114        // This theme uses its own gallery styles.
    115115        add_filter( 'use_default_gallery_style', '__return_false' );
     116
     117        // This theme supports live-updating of widgets in the customizer.
     118        // (3.9 ALPHA - this API may change)
     119        add_theme_support( 'widget-customizer' );
    116120}
    117121endif; // twentyfourteen_setup
    118122add_action( 'after_setup_theme', 'twentyfourteen_setup' );
  • src/wp-content/themes/twentyfourteen/inc/widgets.php

     
    4444                parent::__construct( 'widget_twentyfourteen_ephemera', __( 'Twenty Fourteen Ephemera', 'twentyfourteen' ), array(
    4545                        'classname'   => 'widget_twentyfourteen_ephemera',
    4646                        'description' => __( 'Use this widget to list your recent Aside, Quote, Video, Audio, Image, Gallery, and Link posts', 'twentyfourteen' ),
     47                        'customizer_support' => true,
    4748                ) );
    4849
    4950                /*
  • src/wp-content/themes/twentyfourteen/js/customizer.js

     
    3535                        }
    3636                } );
    3737        } );
     38        // When widget areas are updated, reload the footer.
     39        wp.customize.bind( 'sidebar-updated', function( sidebar_id ) {
     40                if ( 'sidebar-3' === sidebar_id && $.isFunction( $.fn.masonry ) ) {
     41                        var widget_area = $( '#supplementary .widget-area' );
     42                        widget_area.masonry( 'reloadItems' );
     43                        widget_area.masonry();
     44                }
     45        } );
    3846} )( jQuery );
     47 No newline at end of file
  • src/wp-content/themes/twentythirteen/functions.php

     
    105105
    106106        // This theme uses its own gallery styles.
    107107        add_filter( 'use_default_gallery_style', '__return_false' );
     108
     109        // This theme supports live-updating of widgets in the customizer.
     110        // (3.9 ALPHA - this API may change)
     111        add_theme_support( 'widget-customizer' );
    108112}
    109113add_action( 'after_setup_theme', 'twentythirteen_setup' );
    110114
  • src/wp-content/themes/twentythirteen/js/theme-customizer.js

     
    3737                        }
    3838                } );
    3939        } );
     40        // When widget areas are updated, reload the footer.
     41        wp.customize.bind( 'sidebar-updated', function( sidebar_id ) {
     42                if ( 'sidebar-1' === sidebar_id && $.isFunction( $.fn.masonry ) ) {
     43                        var widget_area = $( '#secondary .widget-area' );
     44                        widget_area.masonry( 'reloadItems' );
     45                        widget_area.masonry();
     46                }
     47        } );
    4048} )( jQuery );
  • src/wp-includes/class-wp-customize-control.php

     
    814814                foreach ( $this->default_headers as $choice => $header )
    815815                        $this->print_header_image( $choice, $header );
    816816        }
    817 }
    818  No newline at end of file
     817}
     818
     819/**
     820 * Widget Area Customize Control Class
     821 *
     822 */
     823class WP_Widget_Area_Customize_Control extends WP_Customize_Control {
     824        public $type = 'sidebar_widgets';
     825        public $sidebar_id;
     826
     827        public function to_json() {
     828                parent::to_json();
     829                $exported_properties = array( 'sidebar_id' );
     830                foreach ( $exported_properties as $key ) {
     831                        $this->json[ $key ] = $this->$key;
     832                }
     833        }
     834
     835        public function render_content() {
     836                ?>
     837                <span class="button-secondary add-new-widget" tabindex="0">
     838                        <?php esc_html_e( 'Add a Widget' ); ?>
     839                </span>
     840
     841                <span class="reorder-toggle" tabindex="0">
     842                        <span class="reorder"><?php esc_html_e( 'Reorder' ); ?></span>
     843                        <span class="reorder-done"><?php esc_html_e( 'Done' ); ?></span>
     844                </span>
     845                <?php
     846        }
     847}
     848
     849/**
     850 * Widget Form Customize Control Class
     851 */
     852class WP_Widget_Form_Customize_Control extends WP_Customize_Control {
     853        public $type = 'widget_form';
     854        public $widget_id;
     855        public $widget_id_base;
     856        public $sidebar_id;
     857        public $is_new = false;
     858        public $width;
     859        public $height;
     860        public $is_wide = false;
     861        public $is_live_previewable = false;
     862
     863        public function to_json() {
     864                parent::to_json();
     865                $exported_properties = array( 'widget_id', 'widget_id_base', 'sidebar_id', 'width', 'height', 'is_wide', 'is_live_previewable' );
     866                foreach ( $exported_properties as $key ) {
     867                        $this->json[ $key ] = $this->$key;
     868                }
     869        }
     870
     871        public function render_content() {
     872                global $wp_registered_widgets;
     873                require_once ABSPATH . '/wp-admin/includes/widgets.php';
     874
     875                $widget = $wp_registered_widgets[ $this->widget_id ];
     876                if ( ! isset( $widget['params'][0] ) ) {
     877                        $widget['params'][0] = array();
     878                }
     879
     880                $args = array(
     881                        'widget_id' => $widget['id'],
     882                        'widget_name' => $widget['name'],
     883                );
     884
     885                $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
     886                echo WP_Customize_Widgets::get_widget_control( $args );
     887        }
     888}
     889
  • src/wp-includes/class-wp-customize-manager.php

     
    3131                require( ABSPATH . WPINC . '/class-wp-customize-setting.php' );
    3232                require( ABSPATH . WPINC . '/class-wp-customize-section.php' );
    3333                require( ABSPATH . WPINC . '/class-wp-customize-control.php' );
     34                require( ABSPATH . WPINC . '/class-wp-customize-widgets.php' );
    3435
     36                WP_Customize_Widgets::setup(); // This should be integrated.
     37
    3538                add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) );
    3639
    3740                add_action( 'setup_theme',  array( $this, 'setup_theme' ) );
  • src/wp-includes/class-wp-customize-widgets.php

     
     1<?php
     2/**
     3 * Widget customizer manager class.
     4 */
     5class WP_Customize_Widgets {
     6        const UPDATE_WIDGET_AJAX_ACTION    = 'update_widget';
     7        const RENDER_WIDGET_AJAX_ACTION    = 'render_widget';
     8        const UPDATE_WIDGET_NONCE_POST_KEY = 'update-sidebar-widgets-nonce';
     9        const RENDER_WIDGET_NONCE_POST_KEY = 'render-sidebar-widgets-nonce';
     10        const RENDER_WIDGET_QUERY_VAR      = 'widget_customizer_render_widget';
     11
     12        /**
     13         * All id_bases for widgets defined in core
     14         *
     15         * @var array
     16         */
     17        protected static $core_widget_id_bases = array(
     18                'archives',
     19                'calendar',
     20                'categories',
     21                'links',
     22                'meta',
     23                'nav_menu',
     24                'pages',
     25                'recent-comments',
     26                'recent-posts',
     27                'rss',
     28                'search',
     29                'tag_cloud',
     30                'text',
     31        );
     32
     33        /**
     34         * Initial loader.
     35         */
     36        static function setup() {
     37                add_action( 'after_setup_theme', array( __CLASS__, 'setup_widget_addition_previews' ) );
     38                add_action( 'customize_controls_init', array( __CLASS__, 'customize_controls_init' ) );
     39                add_action( 'customize_register', array( __CLASS__, 'schedule_customize_register' ), 1 );
     40                add_action( sprintf( 'wp_ajax_%s', self::UPDATE_WIDGET_AJAX_ACTION ), array( __CLASS__, 'wp_ajax_update_widget' ) );
     41                add_filter( 'query_vars', array( __CLASS__, 'add_render_widget_query_var' ) );
     42                add_action( 'template_redirect', array( __CLASS__, 'render_widget' ) );
     43                add_action( 'customize_controls_enqueue_scripts', array( __CLASS__, 'customize_controls_enqueue_deps' ) );
     44                add_action( 'customize_controls_print_footer_scripts', array( __CLASS__, 'output_widget_control_templates' ) );
     45                add_action( 'customize_preview_init', array( __CLASS__, 'customize_preview_init' ) );
     46
     47                add_action( 'dynamic_sidebar', array( __CLASS__, 'tally_rendered_widgets' ) );
     48                add_action( 'dynamic_sidebar', array( __CLASS__, 'tally_sidebars_via_dynamic_sidebar_actions' ) );
     49                add_filter( 'temp_is_active_sidebar', array( __CLASS__, 'tally_sidebars_via_is_active_sidebar_calls' ), 10, 2 );
     50                add_filter( 'temp_dynamic_sidebar_has_widgets', array( __CLASS__, 'tally_sidebars_via_dynamic_sidebar_calls' ), 10, 2 );
     51
     52                /**
     53                 * Special filter for Settings Revisions plugin until it can handle
     54                 * dynamically creating settings. Normally this should be handled by
     55                 * a setting's sanitize_js_callback, but when restoring an old revision
     56                 * it may include settings which do not currently exist, and so they
     57                 * do not have the opportunity to be sanitized as needed. Furthermore,
     58                 * we have to add this filter here because the customizer is not
     59                 * initialized in WP Ajax, which is where Settings Revisions currently
     60                 * needs to apply this filter at times.
     61                 */
     62                add_filter( 'temp_customize_sanitize_js', array( __CLASS__, 'temp_customize_sanitize_js' ), 10, 2 );
     63        }
     64
     65        /**
     66         * Get an unslashed post value, or return a default
     67         *
     68         * @param string $name
     69         * @param mixed $default
     70         * @return mixed
     71         */
     72        static function get_post_value( $name, $default = null ) {
     73                if ( ! isset( $_POST[$name] ) ) {
     74                        return $default;
     75                }
     76                return wp_unslash( $_POST[$name] );
     77        }
     78
     79        protected static $_customized;
     80        protected static $_prepreview_added_filters = array();
     81
     82        /**
     83         * Since the widgets get registered (widgets_init) before the customizer settings are set up (customize_register),
     84         * we have to filter the options similarly to how the setting previewer will filter the options later.
     85         *
     86         * @action after_setup_theme
     87         */
     88        static function setup_widget_addition_previews() {
     89                global $wp_customize;
     90                $is_customize_preview = (
     91                        ( ! empty( $wp_customize ) )
     92                        &&
     93                        ( ! is_admin() )
     94                        &&
     95                        ( 'on' === self::get_post_value( 'wp_customize' ) )
     96                        &&
     97                        check_ajax_referer( 'preview-customize_' . $wp_customize->get_stylesheet(), 'nonce', false )
     98                );
     99
     100                $is_ajax_widget_update = (
     101                        ( defined( 'DOING_AJAX' ) && DOING_AJAX )
     102                        &&
     103                        self::get_post_value( 'action' ) === self::UPDATE_WIDGET_AJAX_ACTION
     104                        &&
     105                        check_ajax_referer( self::UPDATE_WIDGET_AJAX_ACTION, self::UPDATE_WIDGET_NONCE_POST_KEY, false )
     106                );
     107
     108                $is_widget_render = (
     109                        isset( $_POST[self::RENDER_WIDGET_QUERY_VAR] )
     110                        &&
     111                        self::get_post_value( 'action' ) === self::RENDER_WIDGET_AJAX_ACTION
     112                        &&
     113                        check_ajax_referer( self::RENDER_WIDGET_AJAX_ACTION, self::RENDER_WIDGET_NONCE_POST_KEY, false )
     114                );
     115
     116                $is_ajax_customize_save = (
     117                        ( defined( 'DOING_AJAX' ) && DOING_AJAX )
     118                        &&
     119                        self::get_post_value( 'action' ) === 'customize_save'
     120                        &&
     121                        check_ajax_referer( 'save-customize_' . $wp_customize->get_stylesheet(), 'nonce' )
     122                );
     123
     124                $is_valid_request = ( $is_ajax_widget_update || $is_widget_render || $is_customize_preview || $is_ajax_customize_save );
     125                if ( ! $is_valid_request ) {
     126                        return;
     127                }
     128
     129                // Input from customizer preview
     130                if ( isset( $_POST['customized'] ) ) {
     131                        $customized = json_decode( self::get_post_value( 'customized' ), true );
     132                }
     133                // Input from ajax widget update request
     134                else {
     135                        $customized    = array();
     136                        $id_base       = self::get_post_value( 'id_base' );
     137                        $widget_number = (int) self::get_post_value( 'widget_number' );
     138                        $option_name   = 'widget_' . $id_base;
     139                        $customized[$option_name] = array();
     140                        if ( false !== $widget_number ) {
     141                                $option_name .= '[' . $widget_number . ']';
     142                                $customized[$option_name][$widget_number] = array();
     143                        }
     144                }
     145
     146                $function = array( __CLASS__, 'prepreview_added_sidebars_widgets' );
     147
     148                $hook = 'option_sidebars_widgets';
     149                add_filter( $hook, $function );
     150                self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
     151
     152                $hook = 'default_option_sidebars_widgets';
     153                add_filter( $hook, $function );
     154                self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
     155
     156                foreach ( $customized as $setting_id => $value ) {
     157                        if ( preg_match( '/^(widget_.+?)(\[(\d+)\])?$/', $setting_id, $matches ) ) {
     158                                $body     = sprintf( 'return %s::prepreview_added_widget_instance( $value, %s );', __CLASS__, var_export( $setting_id, true ) );
     159                                $function = create_function( '$value', $body );
     160                                $option   = $matches[1];
     161
     162                                $hook = sprintf( 'option_%s', $option );
     163                                add_filter( $hook, $function );
     164                                self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
     165
     166                                $hook = sprintf( 'default_option_%s', $option );
     167                                add_filter( $hook, $function );
     168                                self::$_prepreview_added_filters[] = compact( 'hook', 'function' );
     169
     170                                /**
     171                                 * Make sure the option is registered so that the update_option won't fail due to
     172                                 * the filters providing a default value, which causes the update_option() to get confused.
     173                                 */
     174                                add_option( $option, array() );
     175                        }
     176                }
     177
     178                self::$_customized = $customized;
     179        }
     180
     181        /**
     182         * Ensure that newly-added widgets will appear in the widgets_sidebars.
     183         * This is necessary because the customizer's setting preview filters are added after the widgets_init action,
     184         * which is too late for the widgets to be set up properly.
     185         *
     186         * @param array $sidebars_widgets
     187         * @return array
     188         */
     189        static function prepreview_added_sidebars_widgets( $sidebars_widgets ) {
     190                foreach ( self::$_customized as $setting_id => $value ) {
     191                        if ( preg_match( '/^sidebars_widgets\[(.+?)\]$/', $setting_id, $matches ) ) {
     192                                $sidebar_id = $matches[1];
     193                                $sidebars_widgets[$sidebar_id] = $value;
     194                        }
     195                }
     196                return $sidebars_widgets;
     197        }
     198
     199        /**
     200         * Ensure that newly-added widgets will have empty instances so that they will be recognized.
     201         * This is necessary because the customizer's setting preview filters are added after the widgets_init action,
     202         * which is too late for the widgets to be set up properly.
     203         *
     204         * @param array $instance
     205         * @param string $setting_id
     206         * @return array
     207         */
     208        static function prepreview_added_widget_instance( $instance, $setting_id ) {
     209                if ( isset( self::$_customized[$setting_id] ) ) {
     210                        $parsed_setting_id = self::parse_widget_setting_id( $setting_id );
     211                        $widget_number     = $parsed_setting_id['number'];
     212
     213                        // Single widget
     214                        if ( is_null( $widget_number ) ) {
     215                                if ( false === $instance && empty( $value ) ) {
     216                                        $instance = array();
     217                                }
     218                        }
     219                        // Multi widget
     220                        else if ( false === $instance || ! isset( $instance[$widget_number] ) ) {
     221                                if ( empty( $instance ) ) {
     222                                        $instance = array( '_multiwidget' => 1 );
     223                                }
     224                                if ( ! isset( $instance[$widget_number] ) ) {
     225                                        $instance[$widget_number] = array();
     226                                }
     227                        }
     228                }
     229                return $instance;
     230        }
     231
     232        /**
     233         * Remove filters added in setup_widget_addition_previews() which ensure that
     234         * widgets are populating the options during widgets_init
     235         *
     236         * @action wp_loaded
     237         */
     238        static function remove_prepreview_filters() {
     239                foreach ( self::$_prepreview_added_filters as $prepreview_added_filter ) {
     240                        remove_filter( $prepreview_added_filter['hook'], $prepreview_added_filter['function'] );
     241                }
     242                self::$_prepreview_added_filters = array();
     243        }
     244
     245        /**
     246         * Make sure that all widgets get loaded into customizer; these actions are also done in the wp_ajax_save_widget()
     247         *
     248         * @see wp_ajax_save_widget()
     249         * @action customize_controls_init
     250         */
     251        static function customize_controls_init() {
     252                do_action( 'load-widgets.php' );
     253                do_action( 'widgets.php' );
     254                do_action( 'sidebar_admin_setup' );
     255        }
     256
     257        /**
     258         * Add query var so that we can request a widget to be rendered standalone
     259         * on any queried page. This will facilitate rendering widgets if Jetpack's
     260         * Widget Visibility is used, as opposed to rendering a widget via WP Ajax.
     261         *
     262         * @filter query_vars
     263         */
     264        static function add_render_widget_query_var( $query_vars ) {
     265                if ( ! is_admin() ) {
     266                        $query_vars[] = self::RENDER_WIDGET_QUERY_VAR;
     267                }
     268                return $query_vars;
     269        }
     270
     271        /**
     272         * When in preview, invoke customize_register for settings after WordPress is
     273         * loaded so that all filters have been initialized (e.g. Widget Visibility)
     274         */
     275        static function schedule_customize_register( $wp_customize ) {
     276                if ( is_admin() ) { // @todo for some reason, $wp_customize->is_preview() is true here?
     277                        self::customize_register( $wp_customize );
     278                } else {
     279                        add_action( 'wp', array( __CLASS__, 'customize_register' ) );
     280                }
     281        }
     282
     283        static $sidebars_eligible_for_post_message = array();
     284        static $widgets_eligible_for_post_message  = array();
     285
     286        /**
     287         * Register customizer settings and controls for all sidebars and widgets
     288         *
     289         * @action customize_register
     290         */
     291        static function customize_register( $wp_customize = null ) {
     292                global $wp_registered_widgets, $wp_registered_widget_controls;
     293                if ( ! ( $wp_customize instanceof WP_Customize_Manager ) ) {
     294                        $wp_customize = $GLOBALS['wp_customize'];
     295                }
     296
     297                $sidebars_widgets = array_merge(
     298                        array( 'wp_inactive_widgets' => array() ),
     299                        array_fill_keys( array_keys( $GLOBALS['wp_registered_sidebars'] ), array() ),
     300                        wp_get_sidebars_widgets()
     301                );
     302
     303                $new_setting_ids = array();
     304
     305                /**
     306                 * Register a setting for all widgets, including those which are active, inactive, and orphaned
     307                 * since a widget may get suppressed from a sidebar via a plugin (like Widget Visibility).
     308                 */
     309                foreach ( array_keys( $wp_registered_widgets ) as $widget_id ) {
     310                        $setting_id   = self::get_setting_id( $widget_id );
     311                        $setting_args = self::get_setting_args( $setting_id );
     312                        $setting_args['sanitize_callback']    = array( __CLASS__, 'sanitize_widget_instance' );
     313                        $setting_args['sanitize_js_callback'] = array( __CLASS__, 'sanitize_widget_js_instance' );
     314                        $wp_customize->add_setting( $setting_id, $setting_args );
     315                        $new_setting_ids[] = $setting_id;
     316                }
     317
     318                foreach ( $sidebars_widgets as $sidebar_id => $sidebar_widget_ids ) {
     319                        if ( empty( $sidebar_widget_ids ) ) {
     320                                $sidebar_widget_ids = array();
     321                        }
     322                        $is_registered_sidebar = isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] );
     323                        $is_inactive_widgets   = ( 'wp_inactive_widgets' === $sidebar_id );
     324                        $is_active_sidebar     = ( $is_registered_sidebar && ! $is_inactive_widgets );
     325
     326                        /**
     327                         * Add setting for managing the sidebar's widgets
     328                         */
     329                        if ( $is_registered_sidebar || $is_inactive_widgets ) {
     330                                $setting_id   = sprintf( 'sidebars_widgets[%s]', $sidebar_id );
     331                                $setting_args = self::get_setting_args( $setting_id );
     332                                if ( $is_inactive_widgets ) {
     333                                        $setting_args['transport'] = 'postMessage'; // prevent refresh since not rendered anyway
     334                                } else {
     335                                        self::$sidebars_eligible_for_post_message[$sidebar_id] = ( 'postMessage' === self::get_sidebar_widgets_setting_transport( $sidebar_id ) );
     336                                }
     337                                $setting_args['sanitize_callback']    = array( __CLASS__, 'sanitize_sidebar_widgets' );
     338                                $setting_args['sanitize_js_callback'] = array( __CLASS__, 'sanitize_sidebar_widgets_js_instance' );
     339                                $wp_customize->add_setting( $setting_id, $setting_args );
     340                                $new_setting_ids[] = $setting_id;
     341
     342                                /**
     343                                 * Add section to contain controls
     344                                 */
     345                                $section_id = sprintf( 'sidebar-widgets-%s', $sidebar_id );
     346                                if ( $is_active_sidebar ) {
     347                                        $section_args = array(
     348                                                'title' => sprintf( __( 'Widgets: %s' ), $GLOBALS['wp_registered_sidebars'][$sidebar_id]['name'] ),
     349                                                'description' => $GLOBALS['wp_registered_sidebars'][$sidebar_id]['description'],
     350                                        );
     351                                        $section_args = apply_filters( 'customizer_widgets_section_args', $section_args, $section_id, $sidebar_id );
     352                                        $wp_customize->add_section( $section_id, $section_args );
     353
     354                                        $control = new WP_Widget_Area_Customize_Control(
     355                                                $wp_customize,
     356                                                $setting_id,
     357                                                array(
     358                                                        'section' => $section_id,
     359                                                        'sidebar_id' => $sidebar_id,
     360                                                        //'priority' => 99, // so it appears at the end
     361                                                )
     362                                        );
     363                                        $new_setting_ids[] = $setting_id;
     364                                        $wp_customize->add_control( $control );
     365                                }
     366                        }
     367
     368                        /**
     369                         * Add a control for each active widget (located in a sidebar)
     370                         */
     371                        foreach ( $sidebar_widget_ids as $i => $widget_id ) {
     372                                // Skip widgets that may have gone away due to a plugin being deactivated
     373                                if ( ! $is_active_sidebar || ! isset( $GLOBALS['wp_registered_widgets'][$widget_id] ) ) {
     374                                        continue;
     375                                }
     376                                $registered_widget = $GLOBALS['wp_registered_widgets'][$widget_id];
     377                                $setting_id = self::get_setting_id( $widget_id );
     378                                $id_base = $GLOBALS['wp_registered_widget_controls'][$widget_id]['id_base'];
     379                                assert( false !== is_active_widget( $registered_widget['callback'], $registered_widget['id'], false, false ) );
     380                                $control = new WP_Widget_Form_Customize_Control(
     381                                        $wp_customize,
     382                                        $setting_id,
     383                                        array(
     384                                                'label' => $registered_widget['name'],
     385                                                'section' => $section_id,
     386                                                'sidebar_id' => $sidebar_id,
     387                                                'widget_id' => $widget_id,
     388                                                'widget_id_base' => $id_base,
     389                                                'priority' => $i,
     390                                                'width' => $wp_registered_widget_controls[$widget_id]['width'],
     391                                                'height' => $wp_registered_widget_controls[$widget_id]['height'],
     392                                                'is_wide' => self::is_wide_widget( $widget_id ),
     393                                                'is_live_previewable' => self::is_widget_live_previewable( $id_base ),
     394                                        )
     395                                );
     396                                $wp_customize->add_control( $control );
     397                        }
     398                }
     399
     400                /**
     401                 * We have to register these settings later than customize_preview_init so that other
     402                 * filters have had a chance to run.
     403                 * @see self::schedule_customize_register()
     404                 */
     405                if ( did_action( 'customize_preview_init' ) ) {
     406                        foreach ( $new_setting_ids as $new_setting_id ) {
     407                                $wp_customize->get_setting( $new_setting_id )->preview();
     408                        }
     409                }
     410
     411                self::remove_prepreview_filters();
     412        }
     413
     414        /**
     415         * Covert a widget_id into its corresponding customizer setting id (option name)
     416         *
     417         * @param string $widget_id
     418         * @see _get_widget_id_base()
     419         * @return string
     420         */
     421        static function get_setting_id( $widget_id ) {
     422                $parsed_widget_id = self::parse_widget_id( $widget_id );
     423                $setting_id = sprintf( 'widget_%s', $parsed_widget_id['id_base'] );
     424                if ( ! is_null( $parsed_widget_id['number'] ) ) {
     425                        $setting_id .= sprintf( '[%d]', $parsed_widget_id['number'] );
     426                }
     427                return $setting_id;
     428        }
     429
     430        /**
     431         * Core widgets which may have controls wider than 250, but can still be
     432         * shown in the narrow customizer panel. The RSS and Text widgets in Core,
     433         * for example, have widths of 400 and yet they still render fine in the
     434         * customizer panel. This method will return all Core widgets as being
     435         * not wide, but this can be overridden with the is_wide_widget_in_customizer
     436         * filter.
     437         *
     438         * @param string $widget_id
     439         * @return bool
     440         */
     441        static function is_wide_widget( $widget_id ) {
     442                global $wp_registered_widget_controls;
     443                $parsed_widget_id = self::parse_widget_id( $widget_id );
     444                $width = $wp_registered_widget_controls[$widget_id]['width'];
     445                $is_core = in_array( $parsed_widget_id['id_base'], self::$core_widget_id_bases );
     446                $is_wide = ( $width > 250 && ! $is_core );
     447                $is_wide = apply_filters( 'is_wide_widget_in_customizer', $is_wide, $widget_id );
     448                return $is_wide;
     449        }
     450
     451        /**
     452         * Covert a widget ID into its id_base and number components
     453         *
     454         * @param string $widget_id
     455         * @return array
     456         */
     457        static function parse_widget_id( $widget_id ) {
     458                $parsed = array(
     459                        'number' => null,
     460                        'id_base' => null,
     461                );
     462                if ( preg_match( '/^(.+)-(\d+)$/', $widget_id, $matches ) ) {
     463                        $parsed['id_base'] = $matches[1];
     464                        $parsed['number']  = intval( $matches[2] );
     465                } else {
     466                        // likely an old single widget
     467                        $parsed['id_base'] = $widget_id;
     468                }
     469                return $parsed;
     470        }
     471
     472        /**
     473         * Convert a widget setting ID (option path) to its id_base and number components
     474         *
     475         * @throws Widget_Customizer_Exception
     476         * @throws Exception
     477         *
     478         * @param string $setting_id
     479         * @param array
     480         * @return array
     481         */
     482        static function parse_widget_setting_id( $setting_id ) {
     483                if ( ! preg_match( '/^(widget_(.+?))(?:\[(\d+)\])?$/', $setting_id, $matches ) ) {
     484                        throw new Widget_Customizer_Exception( sprintf( 'Invalid widget setting ID: %s', $setting_id ) );
     485                }
     486                $id_base = $matches[2];
     487                $number  = isset( $matches[3] ) ? intval( $matches[3] ) : null;
     488                return compact( 'id_base', 'number' );
     489        }
     490
     491        /**
     492         * Enqueue scripts and styles for customizer panel and export data to JS
     493         *
     494         * @action customize_controls_enqueue_scripts
     495         */
     496        static function customize_controls_enqueue_deps() {
     497                wp_enqueue_script( 'jquery-ui-sortable' );
     498                wp_enqueue_script( 'jquery-ui-droppable' );
     499                wp_enqueue_style(
     500                        'widget-customizer',
     501                        admin_url( 'css/customize-widgets.css' )
     502                );
     503                wp_enqueue_script(
     504                        'widget-customizer',
     505                        admin_url( 'js/customize-widgets.js' ),
     506                        array( 'jquery', 'wp-backbone', 'wp-util', 'customize-controls' )
     507                );
     508
     509                // Export available widgets with control_tpl removed from model
     510                // since plugins need templates to be in the DOM
     511                $available_widgets = array();
     512                foreach ( self::get_available_widgets() as $available_widget ) {
     513                        unset( $available_widget['control_tpl'] );
     514                        $available_widgets[] = $available_widget;
     515                        self::$widgets_eligible_for_post_message[$available_widget['id_base']] = ( 'postMessage' === self::get_widget_setting_transport( $available_widget['id_base'] ) );
     516                }
     517
     518                $widget_reorder_nav_tpl = sprintf(
     519                        '<div class="widget-reorder-nav"><span class="move-widget" tabindex="0" title="%1$s">%2$s</span><span class="move-widget-down" tabindex="0" title="%3$s">%4$s</span><span class="move-widget-up" tabindex="0" title="%5$s">%6$s</span></div>',
     520                        esc_attr__( 'Move to another area...' ),
     521                        esc_html__( 'Move to another area...' ),
     522                        esc_attr__( 'Move down' ),
     523                        esc_html__( 'Move down' ),
     524                        esc_attr__( 'Move up' ),
     525                        esc_html__( 'Move up' )
     526                );
     527
     528                $move_widget_area_tpl = str_replace(
     529                        array( '{description}', '{btn}' ),
     530                        array(
     531                                esc_html__( 'Select an area to move this widget into:' ),
     532                                esc_html__( 'Move' ),
     533                        ),
     534                        '
     535                                <div class="move-widget-area">
     536                                        <p class="description">{description}</p>
     537                                        <ul class="widget-area-select">
     538                                                <% _.each( sidebars, function ( sidebar ){ %>
     539                                                        <li class="" data-id="<%- sidebar.id %>" title="<%- sidebar.description %>" tabindex="0"><%- sidebar.name %></li>
     540                                                <% }); %>
     541                                        </ul>
     542                                        <div class="move-widget-actions">
     543                                                <button class="move-widget-btn button-secondary" type="button">{btn}</button>
     544                                        </div>
     545                                </div>
     546                        '
     547                );
     548
     549                // Why not wp_localize_script? Because we're not localizing, and it forces values into strings
     550                global $wp_scripts;
     551                $exports = array(
     552                        'update_widget_ajax_action' => self::UPDATE_WIDGET_AJAX_ACTION,
     553                        'update_widget_nonce_value' => wp_create_nonce( self::UPDATE_WIDGET_AJAX_ACTION ),
     554                        'update_widget_nonce_post_key' => self::UPDATE_WIDGET_NONCE_POST_KEY,
     555                        'registered_sidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
     556                        'registered_widgets' => $GLOBALS['wp_registered_widgets'],
     557                        'available_widgets' => $available_widgets, // @todo Merge this with registered_widgets
     558                        'i18n' => array(
     559                                'save_btn_label' => _x( 'Apply', 'button to save changes to a widget' ),
     560                                'save_btn_tooltip' => _x( 'Save and preview changes before publishing them.', 'tooltip on the widget save button' ),
     561                                'remove_btn_label' => _x( 'Remove', 'link to move a widget to the inactive widgets sidebar' ),
     562                                'remove_btn_tooltip' => _x( 'Trash widget by moving it to the inactive widgets sidebar.', 'tooltip on btn a widget to move it to the inactive widgets sidebar' ),
     563                        ),
     564                        'tpl' => array(
     565                                'widget_reorder_nav' => $widget_reorder_nav_tpl,
     566                                'move_widget_area' => $move_widget_area_tpl,
     567                        ),
     568                        'sidebars_eligible_for_post_message' => self::$sidebars_eligible_for_post_message,
     569                        'widgets_eligible_for_post_message' => self::$widgets_eligible_for_post_message,
     570                        'current_theme_supports' => current_theme_supports( 'widget-customizer' ),
     571                );
     572                foreach ( $exports['registered_widgets'] as &$registered_widget ) {
     573                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
     574                }
     575
     576                $wp_scripts->add_data(
     577                        'widget-customizer',
     578                        'data',
     579                        sprintf( 'var WidgetCustomizer_exports = %s;', json_encode( $exports ) )
     580                );
     581        }
     582
     583        /**
     584         * Render the widget form control templates into the DOM so that plugin scripts can manipulate them
     585         *
     586         * @action customize_controls_print_footer_scripts
     587         */
     588        static function output_widget_control_templates() {
     589                ?>
     590                <div id="widgets-left"><!-- compatibility with JS which looks for widget templates here -->
     591                <div id="available-widgets">
     592                        <div id="available-widgets-filter">
     593                                <input type="search" placeholder="<?php esc_attr_e( 'Find widgets&hellip;' ) ?>">
     594                        </div>
     595                        <?php foreach ( self::get_available_widgets() as $available_widget ): ?>
     596                                <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">
     597                                        <?php echo $available_widget['control_tpl']; // xss ok ?>
     598                                </div>
     599                        <?php endforeach; ?>
     600                </div><!-- #available-widgets -->
     601                </div><!-- #widgets-left -->
     602                <?php
     603        }
     604
     605        /**
     606         * Get common arguments to supply when constructing a customizer setting
     607         *
     608         * @param string $id
     609         * @param array  [$overrides]
     610         * @return array
     611         */
     612        static function get_setting_args( $id, $overrides = array() ) {
     613                $args = array(
     614                        'type' => 'option',
     615                        'capability' => 'edit_theme_options',
     616                        'transport' => 'refresh',
     617                        'default' => array(),
     618                );
     619                $args = array_merge( $args, $overrides );
     620                $args = apply_filters( 'widget_customizer_setting_args', $args, $id );
     621                return $args;
     622        }
     623
     624        /**
     625         * Make sure that a sidebars_widgets[x] only ever consists of actual widget IDs.
     626         * Used as sanitize_callback for each sidebars_widgets setting.
     627         *
     628         * @param array $widget_ids
     629         * @return array
     630         */
     631        static function sanitize_sidebar_widgets( $widget_ids ) {
     632                global $wp_registered_widgets;
     633                $widget_ids = array_map( 'strval', (array) $widget_ids );
     634                $sanitized_widget_ids = array();
     635                foreach ( $widget_ids as $widget_id ) {
     636                        if ( array_key_exists( $widget_id, $wp_registered_widgets ) ) {
     637                                $sanitized_widget_ids[] = $widget_id;
     638                        }
     639                }
     640                return $sanitized_widget_ids;
     641        }
     642
     643        /**
     644         * Special filter for Settings Revisions plugin until it can handle
     645         * dynamically creating settings.
     646         *
     647         * @param mixed $value
     648         * @param stdClass|WP_Customize_Setting $setting
     649         * @return mixed
     650         */
     651        static function temp_customize_sanitize_js( $value, $setting ) {
     652                if ( preg_match( '/^widget_/', $setting->id ) && $setting->type === 'option' ) {
     653                        $value = self::sanitize_widget_js_instance( $value );
     654                }
     655                return $value;
     656        }
     657
     658        /**
     659         * Get the customizer preview transport for the widget's setting
     660         *
     661         * @param string $id_base
     662         * @return string {refresh|postMessage}
     663         */
     664        static function get_widget_setting_transport( $id_base ) {
     665                if ( ! current_theme_supports( 'widget-customizer' ) || ! self::is_widget_live_previewable( $id_base ) ) {
     666                        return 'refresh';
     667                } else {
     668                        return 'postMessage';
     669                }
     670        }
     671
     672        /**
     673         * Return whether a widget supports being
     674         *
     675         * @param string $id_base
     676         * @return boolean
     677         */
     678        static function is_widget_live_previewable( $id_base ) {
     679                global $wp_registered_widgets, $wp_registered_widget_controls;
     680                $live_previewable = false;
     681
     682                // Core widgets all have built-in support
     683                if ( in_array( $id_base, self::$core_widget_id_bases ) ) {
     684                        $live_previewable = true;
     685                } else {
     686                        // Other widgets can opt-in via the customizer_support widget_option passed to the WP_Widget constructor
     687                        // @todo Should we have a lookup of widgets and their controls by id_base?
     688                        foreach ( $wp_registered_widget_controls as $widget_id => $widget_control ) {
     689                                if ( $widget_control['id_base'] === $id_base ) {
     690                                        assert( isset( $wp_registered_widgets[$widget_id] ) );
     691                                        $live_previewable = ! empty( $wp_registered_widgets[$widget_id]['customizer_support'] );
     692                                        break;
     693                                }
     694                        }
     695                }
     696
     697                $live_previewable = apply_filters( 'customizer_widget_live_previewable', $live_previewable, $id_base );
     698                $live_previewable = apply_filters( "customizer_widget_live_previewable_{$id_base}", $live_previewable );
     699                return $live_previewable;
     700        }
     701
     702        /**
     703         * Get the customizer preview transport for a sidebar
     704         *
     705         * @param string $sidebar_id
     706         * @return string
     707         */
     708        static function get_sidebar_widgets_setting_transport( $sidebar_id ) {
     709                $live_previewable = false;
     710                if ( current_theme_supports( 'widget-customizer' ) ) {
     711                        $live_previewable = true;
     712                }
     713                $live_previewable = apply_filters( 'customizer_sidebar_widgets_live_previewable', $live_previewable, $sidebar_id );
     714                $live_previewable = apply_filters( "customizer_sidebar_widgets_live_previewable_{$sidebar_id}", $live_previewable );
     715                return $live_previewable ? 'postMessage' : 'refresh';
     716        }
     717
     718        /**
     719         * Build up an index of all available widgets for use in Backbone models
     720         *
     721         * @see wp_list_widgets()
     722         * @return array
     723         */
     724        static function get_available_widgets() {
     725                static $available_widgets = array();
     726                if ( ! empty( $available_widgets ) ) {
     727                        return $available_widgets;
     728                }
     729
     730                global $wp_registered_widgets, $wp_registered_widget_controls;
     731                require_once ABSPATH . '/wp-admin/includes/widgets.php'; // for next_widget_id_number()
     732
     733                $sort = $wp_registered_widgets;
     734                usort( $sort, array( __CLASS__, '_sort_name_callback' ) );
     735                $done = array();
     736
     737                foreach ( $sort as $widget ) {
     738                        if ( in_array( $widget['callback'], $done, true ) ) { // We already showed this multi-widget
     739                                continue;
     740                        }
     741
     742                        $sidebar = is_active_widget( $widget['callback'], $widget['id'], false, false );
     743                        $done[]  = $widget['callback'];
     744
     745                        if ( ! isset( $widget['params'][0] ) ) {
     746                                $widget['params'][0] = array();
     747                        }
     748
     749                        $available_widget = $widget;
     750                        unset( $available_widget['callback'] ); // not serializable to JSON
     751
     752                        $args = array(
     753                                'widget_id' => $widget['id'],
     754                                'widget_name' => $widget['name'],
     755                                '_display' => 'template',
     756                        );
     757
     758                        $is_disabled     = false;
     759                        $is_multi_widget = (
     760                                isset( $wp_registered_widget_controls[$widget['id']]['id_base'] )
     761                                &&
     762                                isset( $widget['params'][0]['number'] )
     763                        );
     764                        if ( $is_multi_widget ) {
     765                                $id_base = $wp_registered_widget_controls[$widget['id']]['id_base'];
     766                                $args['_temp_id']   = "$id_base-__i__";
     767                                $args['_multi_num'] = next_widget_id_number( $id_base );
     768                                $args['_add']       = 'multi';
     769                        } else {
     770                                $args['_add'] = 'single';
     771                                if ( $sidebar && 'wp_inactive_widgets' !== $sidebar ) {
     772                                        $is_disabled = true;
     773                                }
     774                                $id_base = $widget['id'];
     775                        }
     776
     777                        $list_widget_controls_args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) );
     778                        $control_tpl = self::get_widget_control( $list_widget_controls_args );
     779
     780                        // The properties here are mapped to the Backbone Widget model
     781                        $available_widget = array_merge(
     782                                $available_widget,
     783                                array(
     784                                        'temp_id' => isset( $args['_temp_id'] ) ? $args['_temp_id'] : null,
     785                                        'is_multi' => $is_multi_widget,
     786                                        'control_tpl' => $control_tpl,
     787                                        'multi_number' => ( $args['_add'] === 'multi' ) ? $args['_multi_num'] : false,
     788                                        'is_disabled' => $is_disabled,
     789                                        'id_base' => $id_base,
     790                                        'transport' => self::get_widget_setting_transport( $id_base ),
     791                                        'width' => $wp_registered_widget_controls[$widget['id']]['width'],
     792                                        'height' => $wp_registered_widget_controls[$widget['id']]['height'],
     793                                        'is_wide' => self::is_wide_widget( $widget['id'] ),
     794                                        'is_live_previewable' => self::is_widget_live_previewable( $id_base ),
     795                                )
     796                        );
     797
     798                        $available_widgets[] = $available_widget;
     799                }
     800                return $available_widgets;
     801        }
     802
     803        /**
     804         * Replace with inline closure once on PHP 5.3:
     805         * sort( $array, function ( $a, $b ) { return strnatcasecmp( $a['name'], $b['name'] ); } );
     806         *
     807         * @access private
     808         */
     809        static function _sort_name_callback( $a, $b ) {
     810                return strnatcasecmp( $a['name'], $b['name'] );
     811        }
     812
     813        /**
     814         * Invoke wp_widget_control() but capture the output buffer and transform the markup
     815         * so that it can be used in the customizer.
     816         *
     817         * @see wp_widget_control()
     818         * @param array $args
     819         * @return string
     820         */
     821        static function get_widget_control( $args ) {
     822                ob_start();
     823                call_user_func_array( 'wp_widget_control', $args );
     824                $replacements = array(
     825                        '<form action="" method="post">' => '<div class="form">',
     826                        '</form>' => '</div><!-- .form -->',
     827                );
     828                $control_tpl = ob_get_clean();
     829                $control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl );
     830                return $control_tpl;
     831        }
     832
     833        /**
     834         * Add hooks for the customizer preview
     835         *
     836         * @action customize_preview_init
     837         */
     838        static function customize_preview_init() {
     839                add_filter( 'sidebars_widgets', array( __CLASS__, 'preview_sidebars_widgets' ), 1 );
     840                add_action( 'wp_enqueue_scripts', array( __CLASS__, 'customize_preview_enqueue_deps' ) );
     841                add_action( 'wp_footer', array( __CLASS__, 'export_preview_data' ), 9999 );
     842        }
     843
     844        /**
     845         * When previewing, make sure the proper previewing widgets are used. Because wp_get_sidebars_widgets()
     846         * gets called early at init (via wp_convert_widget_settings()) and can set global variable
     847         * $_wp_sidebars_widgets to the value of get_option( 'sidebars_widgets' ) before the customizer
     848         * preview filter is added, we have to reset it after the filter has been added.
     849         *
     850         * @filter sidebars_widgets
     851         */
     852        static function preview_sidebars_widgets( $sidebars_widgets ) {
     853                $sidebars_widgets = get_option( 'sidebars_widgets' );
     854                unset( $sidebars_widgets['array_version'] );
     855                return $sidebars_widgets;
     856        }
     857
     858        /**
     859         * Enqueue scripts for the customizer preview
     860         *
     861         * @action wp_enqueue_scripts
     862         */
     863        static function customize_preview_enqueue_deps() {
     864                global $wp_registered_widgets, $wp_registered_widget_controls;
     865
     866                wp_enqueue_script(
     867                        'customize-preview-widgets',
     868                        includes_url( 'js/customize-preview-widgets.js' ),
     869                        array( 'jquery', 'wp-util', 'customize-preview' )
     870                );
     871
     872                /*
     873                wp_enqueue_style(
     874                        'widget-customizer-preview',
     875                        'widget-customizer-preview.css'
     876                );
     877                */
     878
     879                $all_id_bases = array();
     880                foreach ( $wp_registered_widgets as $widget ) {
     881                        if ( isset( $wp_registered_widget_controls[$widget['id']]['id_base'] ) ) {
     882                                $all_id_bases[] = $wp_registered_widget_controls[$widget['id']]['id_base'];
     883                        } else {
     884                                $all_id_bases[] = $widget['id'];
     885                        }
     886                }
     887                $all_id_bases = array_unique( $all_id_bases );
     888                foreach ( $all_id_bases as $id_base ) {
     889                        self::$widgets_eligible_for_post_message[$id_base] = ( 'postMessage' === self::get_widget_setting_transport( $id_base ) );
     890                }
     891
     892                // Why not wp_localize_script? Because we're not localizing, and it forces values into strings
     893                global $wp_scripts;
     894                $exports = array(
     895                        'registered_sidebars' => array_values( $GLOBALS['wp_registered_sidebars'] ),
     896                        'registered_widgets' => $GLOBALS['wp_registered_widgets'],
     897                        'i18n' => array(
     898                                'widget_tooltip' => __( 'Press shift and then click to edit widget in customizer...' ),
     899                        ),
     900                        'render_widget_ajax_action' => self::RENDER_WIDGET_AJAX_ACTION,
     901                        'render_widget_nonce_value' => wp_create_nonce( self::RENDER_WIDGET_AJAX_ACTION ),
     902                        'render_widget_nonce_post_key' => self::RENDER_WIDGET_NONCE_POST_KEY,
     903                        'request_uri' => wp_unslash( $_SERVER['REQUEST_URI'] ),
     904                        'sidebars_eligible_for_post_message' => self::$sidebars_eligible_for_post_message,
     905                        'widgets_eligible_for_post_message' => self::$widgets_eligible_for_post_message,
     906                        'current_theme_supports' => current_theme_supports( 'widget-customizer' ),
     907                );
     908                foreach ( $exports['registered_widgets'] as &$registered_widget ) {
     909                        unset( $registered_widget['callback'] ); // may not be JSON-serializeable
     910                }
     911                $wp_scripts->add_data(
     912                        'customize-preview-widgets',
     913                        'data',
     914                        sprintf( 'var WidgetCustomizerPreview_exports = %s;', json_encode( $exports ) )
     915                );
     916        }
     917
     918        /**
     919         * At the very end of the page, at the very end of the wp_footer, communicate the sidebars that appeared on the page
     920         *
     921         * @action wp_footer
     922         */
     923        static function export_preview_data() {
     924                wp_print_scripts( array( 'customize-preview-widgets' ) );
     925                ?>
     926                <script>
     927                (function () {
     928                        /*global WidgetCustomizerPreview */
     929                        WidgetCustomizerPreview.rendered_sidebars = <?php echo json_encode( array_fill_keys( array_unique( self::$rendered_sidebars ), true ) ) ?>;
     930                        WidgetCustomizerPreview.rendered_widgets = <?php echo json_encode( array_fill_keys( array_keys( self::$rendered_widgets ), true ) ); ?>;
     931                }());
     932                </script>
     933                <?php
     934        }
     935
     936        static protected $rendered_sidebars = array();
     937        static protected $rendered_widgets  = array();
     938
     939        /**
     940         * Keep track of the widgets that were rendered
     941         *
     942         * @action dynamic_sidebar
     943         */
     944        static function tally_rendered_widgets( $widget ) {
     945                self::$rendered_widgets[$widget['id']] = true;
     946        }
     947
     948        /**
     949         * This is hacky. It is too bad that dynamic_sidebar is not just called once with the $sidebar_id supplied
     950         * This does not get called for a sidebar which lacks widgets.
     951         * See core patch which addresses the problem.
     952         *
     953         * @link http://core.trac.wordpress.org/ticket/25368
     954         * @action dynamic_sidebar
     955         */
     956        static function tally_sidebars_via_dynamic_sidebar_actions( $widget ) {
     957                global $sidebars_widgets;
     958                foreach ( $sidebars_widgets as $sidebar_id => $widget_ids ) {
     959                        if ( in_array( $sidebar_id, self::$rendered_sidebars ) ) {
     960                                continue;
     961                        }
     962                        if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) && is_array( $widget_ids ) && in_array( $widget['id'], $widget_ids ) ) {
     963                                self::$rendered_sidebars[] = $sidebar_id;
     964                        }
     965                }
     966        }
     967
     968        /**
     969         * Keep track of the times that is_active_sidebar() is called in the template, and assume that this
     970         * means that the sidebar would be rendered on the template if there were widgets populating it.
     971         *
     972         * @see http://core.trac.wordpress.org/ticket/25368
     973         * @filter temp_is_active_sidebar
     974         */
     975        static function tally_sidebars_via_is_active_sidebar_calls( $is_active, $sidebar_id ) {
     976                if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
     977                        self::$rendered_sidebars[] = $sidebar_id;
     978                }
     979                // We may need to force this to true, and also force-true the value for temp_dynamic_sidebar_has_widgets
     980                // if we want to ensure that there is an area to drop widgets into, if the sidebar is empty.
     981                return $is_active;
     982        }
     983
     984        /**
     985         * Keep track of the times that dynamic_sidebar() is called in the template, and assume that this
     986         * means that the sidebar would be rendered on the template if there were widgets populating it.
     987         *
     988         * @see http://core.trac.wordpress.org/ticket/25368
     989         * @filter temp_dynamic_sidebar_has_widgets
     990         */
     991        static function tally_sidebars_via_dynamic_sidebar_calls( $has_widgets, $sidebar_id ) {
     992                if ( isset( $GLOBALS['wp_registered_sidebars'][$sidebar_id] ) ) {
     993                        self::$rendered_sidebars[] = $sidebar_id;
     994                }
     995                // We may need to force this to true, and also force-true the value for temp_is_active_sidebar
     996                // if we want to ensure that there is an area to drop widgets into, if the sidebar is empty.
     997                return $has_widgets;
     998        }
     999
     1000        /**
     1001         * When the RENDER_WIDGET_QUERY_VAR query_var is supplied, short-circuit the
     1002         * default template from being used and instead render the standalone widget
     1003         * in the context of the original WP query so that things like Jetpack's
     1004         * Widget Visibility work.
     1005         *
     1006         * @uses wp_send_json_success
     1007         * @uses wp_send_json_error
     1008         * @see dynamic_sidebar()
     1009         * @action template_redirect
     1010         */
     1011        static function render_widget() {
     1012                if ( ! get_query_var( self::RENDER_WIDGET_QUERY_VAR ) ) {
     1013                        return;
     1014                }
     1015
     1016                global $wp_registered_widgets, $wp_registered_sidebars;
     1017
     1018                $generic_error = __( 'An error has occurred. Please reload the page and try again.' );
     1019                try {
     1020                        do_action( 'load-widgets.php' );
     1021                        do_action( 'widgets.php' );
     1022
     1023                        $options_transaction = new Options_Transaction();
     1024                        $options_transaction->start();
     1025                        if ( empty( $_POST['widget_id'] ) ) {
     1026                                throw new Widget_Customizer_Exception( __( 'Missing widget_id param' ) );
     1027                        }
     1028                        if ( empty( $_POST['setting_id'] ) ) {
     1029                                throw new Widget_Customizer_Exception( __( 'Missing setting_id param' ) );
     1030                        }
     1031                        if ( empty( $_POST[self::RENDER_WIDGET_NONCE_POST_KEY] ) ) {
     1032                                throw new Widget_Customizer_Exception( __( 'Missing nonce param' ) );
     1033                        }
     1034                        if ( ! check_ajax_referer( self::RENDER_WIDGET_AJAX_ACTION, self::RENDER_WIDGET_NONCE_POST_KEY, false ) ) {
     1035                                throw new Widget_Customizer_Exception( __( 'Nonce check failed. Reload and try again?' ) );
     1036                        }
     1037                        if ( ! current_user_can( 'edit_theme_options' ) ) {
     1038                                throw new Widget_Customizer_Exception( __( 'Current user cannot!' ) );
     1039                        }
     1040                        $widget_id = self::get_post_value( 'widget_id' );
     1041                        if ( ! isset( $wp_registered_widgets[$widget_id] ) ) {
     1042                                throw new Widget_Customizer_Exception( __( 'Unable to find registered widget' ) );
     1043                        }
     1044                        $widget = $wp_registered_widgets[$widget_id];
     1045
     1046                        if ( empty( $_POST['setting'] ) ) {
     1047                                throw new Widget_Customizer_Exception( __( 'Missing instance' ) );
     1048                        }
     1049                        $setting = json_decode( self::get_post_value( 'setting' ), true );
     1050                        if ( is_null( $setting ) ) {
     1051                                throw new Widget_Customizer_Exception( __( 'JSON parse error' ) );
     1052                        }
     1053                        $instance = self::sanitize_widget_instance( $setting );
     1054                        if ( is_null( $instance ) ) {
     1055                                throw new Widget_Customizer_Exception( __( 'Unsanitary widget instance provided' ) );
     1056                        }
     1057
     1058                        $setting_id = self::get_post_value( 'setting_id' );
     1059                        if ( ! preg_match( '/^(.+?)(?:\[(\d+)])?$/', $setting_id, $matches ) ) {
     1060                                throw new Widget_Customizer_Exception( __( 'Malformed setting' ) );
     1061                        }
     1062                        $option_name   = $matches[1];
     1063                        $widget_number = ! empty( $matches[2] ) ? intval( $matches[2] ) : null;
     1064                        $option_value  = get_option( $option_name );
     1065                        if ( is_null( $widget_number ) ) {
     1066                                $option_value = $instance;
     1067                        } else {
     1068                                if ( ! is_array( $option_value ) ) {
     1069                                        $option_value = array();
     1070                                }
     1071                                $option_value[$widget_number] = $instance;
     1072                        }
     1073                        update_option( $option_name, $option_value );
     1074
     1075                        $rendered_widget = null;
     1076                        $sidebar_id = is_active_widget( $widget['callback'], $widget['id'], false, false );
     1077
     1078                        // Render the widget if it is assigned to a sidebar (and not temporarily removed, for example by Widget Visibility)
     1079                        if ( $sidebar_id ) {
     1080                                $sidebar = $wp_registered_sidebars[$sidebar_id];
     1081                                $params  = array_merge(
     1082                                        array(
     1083                                                array_merge(
     1084                                                        $sidebar,
     1085                                                        array(
     1086                                                                'widget_id' => $widget_id,
     1087                                                                'widget_name' => $widget['name'],
     1088                                                        )
     1089                                                ),
     1090                                        ),
     1091                                        (array) $widget['params']
     1092                                );
     1093
     1094                                $callback = $widget['callback'];
     1095
     1096                                // Substitute HTML id and class attributes into before_widget
     1097                                $classname_ = '';
     1098                                foreach ( (array) $widget['classname'] as $cn ) {
     1099                                        if ( is_string( $cn ) ) {
     1100                                                $classname_ .= '_' . $cn;
     1101                                        } else if ( is_object( $cn ) ) {
     1102                                                $classname_ .= '_' . get_class( $cn );
     1103                                        }
     1104                                }
     1105                                $classname_ = ltrim( $classname_, '_' );
     1106
     1107                                $params[0]['before_widget'] = sprintf( $params[0]['before_widget'], $widget_id, $classname_ );
     1108                                $params = apply_filters( 'dynamic_sidebar_params', $params );
     1109
     1110                                // Render the widget
     1111                                ob_start();
     1112                                do_action( 'dynamic_sidebar', $widget );
     1113                                if ( is_callable( $callback ) ) {
     1114                                        call_user_func_array( $callback, $params );
     1115                                }
     1116                                $rendered_widget = ob_get_clean();
     1117                        }
     1118                        $options_transaction->rollback();
     1119                        wp_send_json_success( compact( 'rendered_widget', 'sidebar_id' ) );
     1120                }
     1121                catch ( Exception $e ) {
     1122                        $options_transaction->rollback();
     1123                        if ( $e instanceof Widget_Customizer_Exception ) {
     1124                                $message = $e->getMessage();
     1125                        } else {
     1126                                error_log( sprintf( '%s in %s: %s', get_class( $e ), __FUNCTION__, $e->getMessage() ) );
     1127                                $message = $generic_error;
     1128                        }
     1129                        wp_send_json_error( compact( 'message' ) );
     1130                }
     1131        }
     1132
     1133        /**
     1134         * Serialize an instance and hash it with the AUTH_KEY; when a JS value is
     1135         * posted back to save, this instance hash key is used to ensure that the
     1136         * serialized_instance was not tampered with, but that it had originated
     1137         * from WordPress and so is sanitized.
     1138         *
     1139         * @param array $instance
     1140         * @return string
     1141         */
     1142        protected static function get_instance_hash_key( $instance ) {
     1143                $hash = md5( AUTH_KEY . serialize( $instance ) );
     1144                return $hash;
     1145        }
     1146
     1147        /**
     1148         * Unserialize the JS-instance for storing in the options. It's important
     1149         * that this filter only get applied to an instance once.
     1150         *
     1151         * @see Widget_Customizer::sanitize_widget_js_instance()
     1152         *
     1153         * @param array $value
     1154         * @return array
     1155         */
     1156        static function sanitize_widget_instance( $value ) {
     1157                if ( $value === array() ) {
     1158                        return $value;
     1159                }
     1160                $invalid = (
     1161                        empty( $value['is_widget_customizer_js_value'] )
     1162                        ||
     1163                        empty( $value['instance_hash_key'] )
     1164                        ||
     1165                        empty( $value['encoded_serialized_instance'] )
     1166                );
     1167                if ( $invalid ) {
     1168                        return null;
     1169                }
     1170                $decoded = base64_decode( $value['encoded_serialized_instance'], true );
     1171                if ( false === $decoded ) {
     1172                        return null;
     1173                }
     1174                $instance = unserialize( $decoded );
     1175                if ( false === $instance ) {
     1176                        return null;
     1177                }
     1178                if ( self::get_instance_hash_key( $instance ) !== $value['instance_hash_key'] ) {
     1179                        return null;
     1180                }
     1181                return $instance;
     1182        }
     1183
     1184        /**
     1185         * Convert widget instance into JSON-representable format
     1186         *
     1187         * @see Widget_Customizer::sanitize_widget_instance()
     1188         *
     1189         * @param array $value
     1190         * @return array
     1191         */
     1192        static function sanitize_widget_js_instance( $value ) {
     1193                if ( empty( $value['is_widget_customizer_js_value'] ) ) {
     1194                        $serialized = serialize( $value );
     1195                        $value = array(
     1196                                'encoded_serialized_instance' => base64_encode( $serialized ),
     1197                                'title' => empty( $value['title'] ) ? '' : $value['title'],
     1198                                'is_widget_customizer_js_value' => true,
     1199                                'instance_hash_key' => self::get_instance_hash_key( $value ),
     1200                        );
     1201                }
     1202                return $value;
     1203        }
     1204
     1205        /**
     1206         * Strip out widget IDs for widgets which are no longer registered, such
     1207         * as the case when a plugin orphans a widget in a sidebar when it is deactivated.
     1208         *
     1209         * @param array $widget_ids
     1210         * @return array
     1211         */
     1212        static function sanitize_sidebar_widgets_js_instance( $widget_ids ) {
     1213                global $wp_registered_widgets;
     1214                $widget_ids = array_values( array_intersect( $widget_ids, array_keys( $wp_registered_widgets ) ) );
     1215                return $widget_ids;
     1216        }
     1217
     1218        /**
     1219         * Find and invoke the widget update and control callbacks. Requires that
     1220         * $_POST be populated with the instance data.
     1221         *
     1222         * @throws Widget_Customizer_Exception
     1223         * @throws Exception
     1224         *
     1225         * @param string $widget_id
     1226         * @return array
     1227         */
     1228        static function call_widget_update( $widget_id ) {
     1229                global $wp_registered_widget_updates, $wp_registered_widget_controls;
     1230
     1231                $options_transaction = new Options_Transaction();
     1232
     1233                try {
     1234                        $options_transaction->start();
     1235                        $parsed_id   = self::parse_widget_id( $widget_id );
     1236                        $option_name = 'widget_' . $parsed_id['id_base'];
     1237
     1238                        /**
     1239                         * If a previously-sanitized instance is provided, populate the input vars
     1240                         * with its values so that the widget update callback will read this instance
     1241                         */
     1242                        $added_input_vars = array();
     1243                        if ( ! empty( $_POST['sanitized_widget_setting'] ) ) {
     1244                                $sanitized_widget_setting = json_decode( self::get_post_value( 'sanitized_widget_setting' ), true );
     1245                                if ( empty( $sanitized_widget_setting ) ) {
     1246                                        throw new Widget_Customizer_Exception( 'Malformed sanitized_widget_setting' );
     1247                                }
     1248                                $instance = self::sanitize_widget_instance( $sanitized_widget_setting );
     1249                                if ( is_null( $instance ) ) {
     1250                                        throw new Widget_Customizer_Exception( 'Unsanitary sanitized_widget_setting' );
     1251                                }
     1252                                if ( ! is_null( $parsed_id['number'] ) ) {
     1253                                        $value = array();
     1254                                        $value[$parsed_id['number']] = $instance;
     1255                                        $key = 'widget-' . $parsed_id['id_base'];
     1256                                        $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
     1257                                        $added_input_vars[] = $key;
     1258                                } else {
     1259                                        foreach ( $instance as $key => $value ) {
     1260                                                $_REQUEST[$key] = $_POST[$key] = wp_slash( $value );
     1261                                                $added_input_vars[] = $key;
     1262                                        }
     1263                                }
     1264                        }
     1265
     1266                        /**
     1267                         * Invoke the widget update callback
     1268                         */
     1269                        foreach ( (array) $wp_registered_widget_updates as $name => $control ) {
     1270                                if ( $name === $parsed_id['id_base'] && is_callable( $control['callback'] ) ) {
     1271                                        ob_start();
     1272                                        call_user_func_array( $control['callback'], $control['params'] );
     1273                                        ob_end_clean();
     1274                                        break;
     1275                                }
     1276                        }
     1277
     1278                        // Clean up any input vars that were manually added
     1279                        foreach ( $added_input_vars as $key ) {
     1280                                unset( $_POST[$key] );
     1281                                unset( $_REQUEST[$key] );
     1282                        }
     1283
     1284                        /**
     1285                         * Make sure the expected option was updated
     1286                         */
     1287                        if ( 0 !== $options_transaction->count() ) {
     1288                                if ( count( $options_transaction->options ) > 1 ) {
     1289                                        throw new Widget_Customizer_Exception( sprintf( 'Widget %1$s unexpectedly updated more than one option.', $widget_id ) );
     1290                                }
     1291                                $updated_option_name = key( $options_transaction->options );
     1292                                if ( $updated_option_name !== $option_name ) {
     1293                                        throw new Widget_Customizer_Exception( sprintf( 'Widget %1$s updated option "%2$s", but expected "%3$s".', $widget_id, $updated_option_name, $option_name ) );
     1294                                }
     1295                        }
     1296
     1297                        /**
     1298                         * Obtain the widget control with the updated instance in place
     1299                         */
     1300                        ob_start();
     1301                        $form = $wp_registered_widget_controls[$widget_id];
     1302                        if ( $form ) {
     1303                                call_user_func_array( $form['callback'], $form['params'] );
     1304                        }
     1305                        $form = ob_get_clean();
     1306
     1307                        /**
     1308                         * Obtain the widget instance
     1309                         */
     1310                        $option = get_option( $option_name );
     1311                        if ( null !== $parsed_id['number'] ) {
     1312                                $instance = $option[$parsed_id['number']];
     1313                        } else {
     1314                                $instance = $option;
     1315                        }
     1316
     1317                        $options_transaction->rollback();
     1318                        return compact( 'instance', 'form' );
     1319                }
     1320                catch ( Exception $e ) {
     1321                        $options_transaction->rollback();
     1322                        throw $e;
     1323                }
     1324        }
     1325
     1326        /**
     1327         * Allow customizer to update a widget using its form, but return the new
     1328         * instance info via Ajax instead of saving it to the options table.
     1329         * Most code here copied from wp_ajax_save_widget()
     1330         *
     1331         * @see wp_ajax_save_widget
     1332         * @todo Reuse wp_ajax_save_widget now that we have option transactions?
     1333         * @action wp_ajax_update_widget
     1334         */
     1335        static function wp_ajax_update_widget() {
     1336                $generic_error = __( 'An error has occurred. Please reload the page and try again.' );
     1337
     1338                try {
     1339                        if ( ! check_ajax_referer( self::UPDATE_WIDGET_AJAX_ACTION, self::UPDATE_WIDGET_NONCE_POST_KEY, false ) ) {
     1340                                throw new Widget_Customizer_Exception( __( 'Nonce check failed. Reload and try again?' ) );
     1341                        }
     1342                        if ( ! current_user_can( 'edit_theme_options' ) ) {
     1343                                throw new Widget_Customizer_Exception( __( 'Current user cannot!' ) );
     1344                        }
     1345                        if ( ! isset( $_POST['widget-id'] ) ) {
     1346                                throw new Widget_Customizer_Exception( __( 'Incomplete request' ) );
     1347                        }
     1348
     1349                        unset( $_POST[self::UPDATE_WIDGET_NONCE_POST_KEY], $_POST['action'] );
     1350
     1351                        do_action( 'load-widgets.php' );
     1352                        do_action( 'widgets.php' );
     1353                        do_action( 'sidebar_admin_setup' );
     1354
     1355                        $widget_id = self::get_post_value( 'widget-id' );
     1356                        $parsed_id = self::parse_widget_id( $widget_id );
     1357                        $id_base   = $parsed_id['id_base'];
     1358
     1359                        if ( isset( $_POST['widget-' . $id_base] ) && is_array( $_POST['widget-' . $id_base] ) && preg_match( '/__i__|%i%/', key( $_POST['widget-' . $id_base] ) ) ) {
     1360                                throw new Widget_Customizer_Exception( 'Cannot pass widget templates to create new instances; apply template vars in JS' );
     1361                        }
     1362
     1363                        $updated_widget = self::call_widget_update( $widget_id ); // => {instance,form}
     1364                        $form = $updated_widget['form'];
     1365                        $instance = self::sanitize_widget_js_instance( $updated_widget['instance'] );
     1366
     1367                        wp_send_json_success( compact( 'form', 'instance' ) );
     1368                }
     1369                catch( Exception $e ) {
     1370                        if ( $e instanceof Widget_Customizer_Exception ) {
     1371                                $message = $e->getMessage();
     1372                        } else {
     1373                                error_log( sprintf( '%s in %s: %s', get_class( $e ), __FUNCTION__, $e->getMessage() ) );
     1374                                $message = $generic_error;
     1375                        }
     1376                        wp_send_json_error( compact( 'message' ) );
     1377                }
     1378        }
     1379}
     1380
     1381class Widget_Customizer_Exception extends Exception {}
     1382
     1383class Options_Transaction {
     1384
     1385        /**
     1386         * @var array $options values updated while transaction is open
     1387         */
     1388        public $options = array();
     1389
     1390        protected $_ignore_transients = true;
     1391        protected $_is_current = false;
     1392        protected $_operations = array();
     1393
     1394        function __construct( $ignore_transients = true ) {
     1395                $this->_ignore_transients = $ignore_transients;
     1396        }
     1397
     1398        /**
     1399         * Determine whether or not the transaction is open
     1400         * @return bool
     1401         */
     1402        function is_current() {
     1403                return $this->_is_current;
     1404        }
     1405
     1406        /**
     1407         * @param $option_name
     1408         * @return boolean
     1409         */
     1410        function is_option_ignored( $option_name ) {
     1411                return ( $this->_ignore_transients && 0 === strpos( $option_name, '_transient_' ) );
     1412        }
     1413
     1414        /**
     1415         * Get the number of operations performed in the transaction
     1416         * @return bool
     1417         */
     1418        function count() {
     1419                return count( $this->_operations );
     1420        }
     1421
     1422        /**
     1423         * Start keeping track of changes to options, and cache their new values
     1424         */
     1425        function start() {
     1426                $this->_is_current = true;
     1427                add_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
     1428                add_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
     1429                add_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
     1430                add_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
     1431        }
     1432
     1433        /**
     1434         * @action added_option
     1435         * @param $option_name
     1436         * @param $new_value
     1437         */
     1438        function _capture_added_option( $option_name, $new_value ) {
     1439                if ( $this->is_option_ignored( $option_name ) ) {
     1440                        return;
     1441                }
     1442                $this->options[$option_name] = $new_value;
     1443                $operation = 'add';
     1444                $this->_operations[] = compact( 'operation', 'option_name', 'new_value' );
     1445        }
     1446
     1447        /**
     1448         * @action updated_option
     1449         * @param string $option_name
     1450         * @param mixed $old_value
     1451         * @param mixed $new_value
     1452         */
     1453        function _capture_updated_option( $option_name, $old_value, $new_value ) {
     1454                if ( $this->is_option_ignored( $option_name ) ) {
     1455                        return;
     1456                }
     1457                $this->options[$option_name] = $new_value;
     1458                $operation = 'update';
     1459                $this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'new_value' );
     1460        }
     1461
     1462        protected $_pending_delete_option_autoload;
     1463        protected $_pending_delete_option_value;
     1464
     1465        /**
     1466         * It's too bad the old_value and autoload aren't passed into the deleted_option action
     1467         * @action delete_option
     1468         * @param string $option_name
     1469         */
     1470        function _capture_pre_deleted_option( $option_name ) {
     1471                if ( $this->is_option_ignored( $option_name ) ) {
     1472                        return;
     1473                }
     1474                global $wpdb;
     1475                $autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s", $option_name ) ); // db call ok; no-cache ok
     1476                $this->_pending_delete_option_autoload = $autoload;
     1477                $this->_pending_delete_option_value    = get_option( $option_name );
     1478        }
     1479
     1480        /**
     1481         * @action deleted_option
     1482         * @param string $option_name
     1483         */
     1484        function _capture_deleted_option( $option_name ) {
     1485                if ( $this->is_option_ignored( $option_name ) ) {
     1486                        return;
     1487                }
     1488                unset( $this->options[$option_name] );
     1489                $operation = 'delete';
     1490                $old_value = $this->_pending_delete_option_value;
     1491                $autoload  = $this->_pending_delete_option_autoload;
     1492                $this->_operations[] = compact( 'operation', 'option_name', 'old_value', 'autoload' );
     1493        }
     1494
     1495        /**
     1496         * Undo any changes to the options since start() was called
     1497         */
     1498        function rollback() {
     1499                remove_action( 'updated_option', array( $this, '_capture_updated_option' ), 10, 3 );
     1500                remove_action( 'added_option', array( $this, '_capture_added_option' ), 10, 2 );
     1501                remove_action( 'delete_option', array( $this, '_capture_pre_deleted_option' ), 10, 1 );
     1502                remove_action( 'deleted_option', array( $this, '_capture_deleted_option' ), 10, 1 );
     1503                while ( 0 !== count( $this->_operations ) ) {
     1504                        $option_operation = array_pop( $this->_operations );
     1505                        if ( 'add' === $option_operation['operation'] ) {
     1506                                delete_option( $option_operation['option_name'] );
     1507                        }
     1508                        else if ( 'delete' === $option_operation['operation'] ) {
     1509                                add_option( $option_operation['option_name'], $option_operation['old_value'], null, $option_operation['autoload'] );
     1510                        }
     1511                        else if ( 'update' === $option_operation['operation'] ) {
     1512                                update_option( $option_operation['option_name'], $option_operation['old_value'] );
     1513                        }
     1514                        else {
     1515                                throw new Exception( 'Unexpected operation' );
     1516                        }
     1517                }
     1518                $this->_is_current = false;
     1519        }
     1520}
  • src/wp-includes/js/customize-preview-widgets.js

     
     1/*global jQuery, WidgetCustomizerPreview_exports, _ */
     2/*exported WidgetCustomizerPreview */
     3var WidgetCustomizerPreview = (function ($) {
     4        'use strict';
     5
     6        var self = {
     7                rendered_sidebars: {}, // @todo Make rendered a property of the Backbone model
     8                sidebars_eligible_for_post_message: {},
     9                rendered_widgets: {}, // @todo Make rendered a property of the Backbone model
     10                widgets_eligible_for_post_message: {},
     11                registered_sidebars: [], // @todo Make a Backbone collection
     12                registered_widgets: {}, // @todo Make array, Backbone collection
     13                widget_selectors: [],
     14                render_widget_ajax_action: null,
     15                render_widget_nonce_value: null,
     16                render_widget_nonce_post_key: null,
     17                preview: null,
     18                i18n: {},
     19
     20                init: function () {
     21                        this.buildWidgetSelectors();
     22                        this.highlightControls();
     23                        this.livePreview();
     24
     25                        self.preview.bind( 'active', function() {
     26                                self.preview.send( 'rendered-sidebars', self.rendered_sidebars ); // @todo Only send array of IDs
     27                                self.preview.send( 'rendered-widgets', self.rendered_widgets ); // @todo Only send array of IDs
     28                        } );
     29                },
     30
     31                /**
     32                 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info
     33                 */
     34                buildWidgetSelectors: function () {
     35                        $.each( self.registered_sidebars, function ( i, sidebar ) {
     36                                var widget_tpl = [
     37                                        sidebar.before_widget.replace('%1$s', '').replace('%2$s', ''),
     38                                        sidebar.before_title,
     39                                        sidebar.after_title,
     40                                        sidebar.after_widget
     41                                ].join('');
     42                                var empty_widget = $(widget_tpl);
     43                                var widget_selector = empty_widget.prop('tagName');
     44                                var widget_classes = empty_widget.prop('className').replace(/^\s+|\s+$/g, '');
     45                                if ( widget_classes ) {
     46                                        widget_selector += '.' + widget_classes.split(/\s+/).join('.');
     47                                }
     48                                self.widget_selectors.push(widget_selector);
     49                        });
     50                },
     51
     52                /**
     53                 * Obtain a widget instance if it was added to the provided sidebar
     54                 * This addresses a race condition where a widget is moved between sidebars
     55                 * We cannot use ID selector because jQuery will only return the first one
     56                 * that matches. We have to resort to an [id] attribute selector
     57                 *
     58                 * @param {String} sidebar_id
     59                 * @param {String} widget_id
     60                 * @return {jQuery}
     61                 */
     62                getSidebarWidgetElement: function ( sidebar_id, widget_id ) {
     63                        return $( '[id=' + widget_id + ']' ).filter( function () {
     64                                return $( this ).data( 'widget_customizer_sidebar_id' ) === sidebar_id;
     65                        } );
     66                },
     67
     68                /**
     69                 *
     70                 */
     71                highlightControls: function() {
     72
     73                        var selector = this.widget_selectors.join(',');
     74
     75                        $(selector).attr( 'title', self.i18n.widget_tooltip );
     76
     77                        $(document).on( 'mouseenter', selector, function () {
     78                                var control = parent.WidgetCustomizer.getWidgetFormControlForWidget( $(this).prop('id') );
     79                                if ( control ) {
     80                                        control.highlightSectionAndControl();
     81                                }
     82                        });
     83
     84                        // Open expand the widget control when shift+clicking the widget element
     85                        $(document).on( 'click', selector, function ( e ) {
     86                                if ( ! e.shiftKey ) {
     87                                        return;
     88                                }
     89                                e.preventDefault();
     90                                var control = parent.WidgetCustomizer.getWidgetFormControlForWidget( $(this).prop('id') );
     91                                if ( control ) {
     92                                        control.focus();
     93                                }
     94                        });
     95                },
     96
     97                /**
     98                 * if the containing sidebar is eligible, and if there are sibling widgets the sidebar currently rendered
     99                 * @param {String} sidebar_id
     100                 * @return {Boolean}
     101                 */
     102                sidebarCanLivePreview: function ( sidebar_id ) {
     103                        if ( ! self.current_theme_supports ) {
     104                                return false;
     105                        }
     106                        if ( ! self.sidebars_eligible_for_post_message[sidebar_id] ) {
     107                                return false;
     108                        }
     109                        var widget_ids = wp.customize( sidebar_id_to_setting_id( sidebar_id ) )();
     110                        var rendered_widget_ids = _( widget_ids ).filter( function ( widget_id ) {
     111                                return 0 !== self.getSidebarWidgetElement( sidebar_id, widget_id ).length;
     112                        } );
     113                        if ( rendered_widget_ids.length === 0 ) {
     114                                return false;
     115                        }
     116                        return true;
     117                },
     118
     119
     120                /**
     121                 * We can only know if a sidebar can be live-previewed by letting the
     122                 * preview tell us, so this updates the parent's transports to
     123                 * postMessage when it is available. If there is a switch from
     124                 * postMessage to refresh, the preview window will request a refresh.
     125                 * @param {String} sidebar_id
     126                 */
     127                refreshTransports: function () {
     128                        var changed_to_refresh = false;
     129                        $.each( self.rendered_sidebars, function ( sidebar_id ) {
     130                                var setting_id = sidebar_id_to_setting_id( sidebar_id );
     131                                var setting = parent.wp.customize( setting_id );
     132                                var sidebar_transport = self.sidebarCanLivePreview( sidebar_id ) ? 'postMessage' : 'refresh';
     133                                if ( 'refresh' === sidebar_transport && 'postMessage' === setting.transport ) {
     134                                        changed_to_refresh = true;
     135                                }
     136                                setting.transport = sidebar_transport;
     137
     138                                var widget_ids = wp.customize( setting_id )();
     139                                $.each( widget_ids, function ( i, widget_id ){
     140                                        var setting_id = widget_id_to_setting_id( widget_id );
     141                                        var setting = parent.wp.customize( setting_id );
     142                                        var widget_transport = 'refresh';
     143                                        var id_base = widget_id_to_base( widget_id );
     144                                        if ( self.current_theme_supports && sidebar_transport === 'postMessage' && self.widgets_eligible_for_post_message[id_base] ) {
     145                                                widget_transport = 'postMessage';
     146                                        }
     147                                        if ( 'refresh' === widget_transport && 'postMessage' === setting.transport ) {
     148                                                changed_to_refresh = true;
     149                                        }
     150                                        setting.transport = widget_transport;
     151                                } );
     152                        } );
     153                        if ( changed_to_refresh ) {
     154                                self.preview.send( 'refresh' );
     155                        }
     156                },
     157
     158                /**
     159                 * Set up the ability for the widget to be previewed without doing a preview refresh
     160                 */
     161                livePreview: function () {
     162                        var already_bound_widgets = {};
     163
     164                        var bind_widget_setting = function( widget_id ) {
     165                                var setting_id = widget_id_to_setting_id( widget_id );
     166                                var binder = function( value ) {
     167                                        already_bound_widgets[widget_id] = true;
     168                                        value.bind( function( to, from ) {
     169                                                // Workaround for http://core.trac.wordpress.org/ticket/26061;
     170                                                // once fixed, this conditional can be eliminated
     171                                                if ( _.isEqual( from, to ) ) {
     172                                                        return;
     173                                                }
     174
     175                                                var widget_setting_id = widget_id_to_setting_id( widget_id );
     176                                                if ( parent.wp.customize( widget_setting_id ).transport !== 'postMessage' ) {
     177                                                        return;
     178                                                }
     179
     180                                                var customized = {};
     181                                                var sidebar_id = null;
     182                                                wp.customize.each( function ( setting, setting_id ) {
     183                                                        var matches = setting_id.match( /^sidebars_widgets\[(.+)\]/ );
     184                                                        if ( ! matches ) {
     185                                                                return;
     186                                                        }
     187                                                        var other_sidebar_id = matches[1];
     188                                                        if ( setting().indexOf( widget_id ) !== -1 ) {
     189                                                                sidebar_id = other_sidebar_id;
     190                                                        }
     191                                                        customized[sidebar_id_to_setting_id( other_sidebar_id )] = setting();
     192                                                } );
     193                                                if ( ! sidebar_id ) {
     194                                                        throw new Error( 'Widget does not exist in a sidebar.' );
     195                                                }
     196
     197                                                var data = {
     198                                                        widget_customizer_render_widget: 1,
     199                                                        action: self.render_widget_ajax_action,
     200                                                        widget_id: widget_id,
     201                                                        setting_id: setting_id,
     202                                                        setting: JSON.stringify( to )
     203                                                };
     204
     205                                                customized[setting_id] = to;
     206                                                data.customized = JSON.stringify(customized);
     207                                                data[self.render_widget_nonce_post_key] = self.render_widget_nonce_value;
     208
     209                                                console.log( self.request_uri, data );
     210                                                $.post( self.request_uri, data, function ( r ) {
     211                                                        if ( ! r.success ) {
     212                                                                throw new Error( r.data && r.data.message ? r.data.message : 'FAIL' );
     213                                                        }
     214
     215                                                        var old_widget = self.getSidebarWidgetElement( sidebar_id, widget_id );
     216                                                        var new_widget = $( r.data.rendered_widget );
     217                                                        new_widget.data( 'widget_customizer_sidebar_id', sidebar_id );
     218                                                        if ( new_widget.length && old_widget.length ) {
     219                                                                old_widget.replaceWith( new_widget );
     220                                                        } else if ( ! new_widget.length && old_widget.length ) {
     221                                                                old_widget.remove();
     222                                                        } else if ( new_widget.length && ! old_widget.length ) {
     223                                                                var sidebar_widgets = wp.customize( sidebar_id_to_setting_id( r.data.sidebar_id ) )();
     224                                                                var position = sidebar_widgets.indexOf( widget_id );
     225                                                                if ( -1 === position ) {
     226                                                                        throw new Error( 'Unable to determine new widget position in sidebar' );
     227                                                                }
     228                                                                if ( sidebar_widgets.length === 1 ) {
     229                                                                        throw new Error( 'Unexpected postMessage for adding first widget to sidebar; refresh must be used instead.' );
     230                                                                }
     231
     232                                                                var get_widget_elements = function ( widget_ids ) {
     233                                                                        var widget_elements = [];
     234                                                                        _( widget_ids ).each( function ( widget_id ) {
     235                                                                                var widget = self.getSidebarWidgetElement( sidebar_id, widget_id );
     236                                                                                if ( widget.length ) {
     237                                                                                        widget_elements.push( widget[0] );
     238                                                                                }
     239                                                                        } );
     240                                                                        return widget_elements;
     241                                                                };
     242
     243                                                                var before_widget_ids = ( position !== 0 ? sidebar_widgets.slice( 0, position ) : [] );
     244                                                                var before_widgets = jQuery().add( get_widget_elements( before_widget_ids ) );
     245                                                                var before_widget = before_widgets.last();
     246
     247                                                                var after_widget_ids = sidebar_widgets.slice( position + 1 );
     248                                                                var after_widgets = jQuery().add( get_widget_elements( after_widget_ids ) );
     249                                                                var after_widget = after_widgets.first();
     250
     251                                                                if ( before_widget.length ) {
     252                                                                        before_widget.after( new_widget );
     253                                                                } else if ( after_widget.length ) {
     254                                                                        after_widget.before( new_widget );
     255                                                                } else {
     256                                                                        throw new Error( 'Unable to locate adjacent widget in sidebar.' );
     257                                                                }
     258                                                        }
     259
     260                                                        // Update widget visibility
     261                                                        self.rendered_widgets[widget_id] = ( 0 !== self.getSidebarWidgetElement( sidebar_id, widget_id ).length );
     262
     263                                                        self.preview.send( 'rendered-widgets', self.rendered_widgets );
     264                                                        self.preview.send( 'widget-updated', widget_id );
     265                                                        wp.customize.trigger( 'sidebar-updated', sidebar_id );
     266                                                        wp.customize.trigger( 'widget-updated', widget_id );
     267                                                        self.refreshTransports();
     268                                                } );
     269                                        } );
     270                                };
     271                                wp.customize( setting_id, binder );
     272                                already_bound_widgets[setting_id] = binder;
     273                        };
     274
     275                        $.each( self.rendered_sidebars, function ( sidebar_id ) {
     276                                var setting_id = sidebar_id_to_setting_id( sidebar_id );
     277                                wp.customize( setting_id, function( value ) {
     278                                        // Initially keep track of the sidebars with which widgets are associated.
     279                                        // Henceforth we must always scope the widget_id by the associated sidebar_id (see self.getSidebarWidgetElement)
     280                                        _( value() ).each( function ( widget_id ) {
     281                                                $( '#' + widget_id ).data( 'widget_customizer_sidebar_id', sidebar_id );
     282                                        } );
     283
     284                                        value.bind( function( to, from ) {
     285                                                // Workaround for http://core.trac.wordpress.org/ticket/26061;
     286                                                // once fixed, this conditional can be eliminated
     287                                                if ( _.isEqual( from, to ) ) {
     288                                                        return;
     289                                                }
     290
     291                                                // Delete the widget from the DOM if it no longer exists in the sidebar
     292                                                $.each( from, function ( i, old_widget_id ) {
     293                                                        if ( -1 === to.indexOf( old_widget_id ) ) {
     294                                                                self.getSidebarWidgetElement( sidebar_id, old_widget_id ).remove();
     295                                                        }
     296                                                } );
     297
     298                                                // Sort widgets: reorder relative to the first widget rendered
     299                                                var first_rendered_widget_id = _( to ).find( function ( widget_id ) {
     300                                                        return 0 !== self.getSidebarWidgetElement( sidebar_id, widget_id ).length;
     301                                                } );
     302                                                var first_rendered_widget = self.getSidebarWidgetElement( sidebar_id, first_rendered_widget_id );
     303                                                _.chain( to.slice(0) ).reverse().each( function ( widget_id ) {
     304                                                        if ( first_rendered_widget_id !== widget_id ) {
     305                                                                var widget = self.getSidebarWidgetElement( sidebar_id, widget_id );
     306                                                                first_rendered_widget.after( widget );
     307                                                        }
     308                                                } );
     309
     310                                                // Create settings for newly-created widgets
     311                                                $.each( to, function ( i, widget_id ) {
     312                                                        var setting_id = widget_id_to_setting_id( widget_id );
     313                                                        var setting = wp.customize( setting_id );
     314                                                        if ( ! setting ) {
     315                                                                setting = wp.customize.create( setting_id, {} );
     316                                                        }
     317
     318                                                        // @todo Is there another way to check if we bound?
     319                                                        if ( ! already_bound_widgets[widget_id] ) {
     320                                                                bind_widget_setting( widget_id );
     321                                                        }
     322
     323                                                        // Force the callback to fire if this widget is newly-added
     324                                                        if ( from.indexOf( widget_id ) === -1 ) {
     325                                                                self.refreshTransports();
     326                                                                var parent_setting = parent.wp.customize( setting_id );
     327                                                                if ( 'postMessage' === parent_setting.transport ) {
     328                                                                        setting.callbacks.fireWith( setting, [ setting(), null ] );
     329                                                                } else {
     330                                                                        self.preview.send( 'refresh' );
     331                                                                }
     332                                                        }
     333                                                } );
     334
     335                                                // If a widget was removed so that no widgets remain rendered in sidebar, we need to disable postMessage
     336                                                self.refreshTransports();
     337                                                wp.customize.trigger( 'sidebar-updated', sidebar_id );
     338                                        } );
     339                                } );
     340                        } );
     341
     342                        $.each( self.registered_widgets, function ( widget_id ) {
     343                                var setting_id = widget_id_to_setting_id( widget_id );
     344                                if ( ! wp.customize.has( setting_id ) ) {
     345                                        // Used to have to do this: wp.customize.create( setting_id, instance );
     346                                        // Now that the settings are registered at the `wp` action, it is late enough
     347                                        // for all filters to be added, e.g. sidebars_widgets for Widget Visibility
     348                                        throw new Error( 'Expected customize to have registered setting for widget ' + widget_id );
     349                                }
     350                                bind_widget_setting( widget_id );
     351                        } );
     352
     353                        // Opt-in to LivePreview
     354                        self.refreshTransports();
     355                }
     356        };
     357
     358        $.extend(self, WidgetCustomizerPreview_exports);
     359
     360        /**
     361         * Capture the instance of the Preview since it is private
     362         */
     363        var OldPreview = wp.customize.Preview;
     364        wp.customize.Preview = OldPreview.extend( {
     365                initialize: function( params, options ) {
     366                        self.preview = this;
     367                        OldPreview.prototype.initialize.call( this, params, options );
     368                }
     369        } );
     370
     371        /**
     372         * @param {String} widget_id
     373         * @returns {String}
     374         */
     375        function widget_id_to_setting_id( widget_id ) {
     376                var setting_id = null;
     377                var matches = widget_id.match(/^(.+?)(?:-(\d+)?)$/);
     378                if ( matches ) {
     379                        setting_id = 'widget_' + matches[1] + '[' + matches[2] + ']';
     380                } else {
     381                        setting_id = 'widget_' + widget_id;
     382                }
     383                return setting_id;
     384        }
     385
     386        /**
     387         * @param {String} widget_id
     388         * @returns {String}
     389         */
     390        function widget_id_to_base( widget_id ) {
     391                return widget_id.replace( /-\d+$/, '' );
     392        }
     393
     394        /**
     395         * @param {String} sidebar_id
     396         * @returns {string}
     397         */
     398        function sidebar_id_to_setting_id( sidebar_id ) {
     399                return 'sidebars_widgets[' + sidebar_id + ']';
     400        }
     401
     402        // @todo on customize ready?
     403        $(function () {
     404                self.init();
     405        });
     406
     407        return self;
     408}( jQuery ));
  • tests/phpunit/tests/widgets/customizer.php

     
     1<?php
     2class Test_Widget_Customizer extends WP_UnitTestCase {
     3
     4        public function setUp() {
     5                parent::setUp();
     6
     7                // Create a new user then add 'edit_theme_options' capability
     8                $user_id = $this->factory->user->create();
     9                $user    = wp_set_current_user( $user_id );
     10                $user->add_cap( 'edit_theme_options' );
     11
     12                // Pretending in customize page.
     13                if ( ! isset( $_REQUEST['wp_customize'] ) ) {
     14                        $_REQUEST['wp_customize'] = 'on';
     15                }
     16
     17                if ( ! class_exists( 'WP_Customize_Manager' ) )
     18                        require_once ABSPATH . WPINC . '/class-wp-customize-manager.php';
     19
     20                // Init Customize class.
     21                if ( ! isset( $GLOBALS['wp_customize'] ) )
     22                        $GLOBALS['wp_customize'] = new WP_Customize_Manager;
     23
     24                // Removes any registered actions (in which some themes use) and re-register action
     25                // from this plugin.
     26                remove_all_actions( 'customize_register' );
     27                add_action( 'customize_register', array( 'Widget_Customizer', 'customize_register' ) );
     28
     29                set_current_screen( 'customize' );
     30
     31                do_action( 'customize_register', $GLOBALS['wp_customize'] );
     32        }
     33
     34        public function test_plugins_loaded() {
     35                $this->assertTrue( class_exists( 'Widget_Customizer' ), 'class Widget_Customizer does not exists' );
     36                $this->assertGreaterThan( 0, has_action( 'plugins_loaded', array( 'Widget_Customizer', 'setup' ) ), 'setup method is not properly invoked during plugins_loaded' );
     37        }
     38
     39        public function test_setup_i18n() {
     40                global $l10n;
     41
     42                $test_string = __( 'Test string', 'widget-customizer' );
     43                $this->assertArrayHasKey( 'widget-customizer', $l10n, 'Text domain is not loaded or has the wrong name' );
     44        }
     45
     46        public function test_setup_actions() {
     47                // Makes sure all registered actions are invoked in expected hooks
     48
     49                $this->assertGreaterThan( 0, has_action( 'after_setup_theme', array( 'Widget_Customizer', 'setup_widget_addition_previews' ) ), 'preview_new_widgets method is not properly invoked during after_setup_theme' );
     50
     51                $this->assertGreaterThan( 0, has_action( 'customize_register', array( 'Widget_Customizer', 'customize_register' ) ), 'customize_register method is not properly invoked during customize_register' );
     52
     53                $this->assertGreaterThan( 0, has_action( sprintf( 'wp_ajax_%s', Widget_Customizer::UPDATE_WIDGET_AJAX_ACTION ), array( 'Widget_Customizer', 'wp_ajax_update_widget' ) ), 'wp_ajax_update_widget method is not properly invoked during wp_ajax_' . Widget_Customizer::UPDATE_WIDGET_AJAX_ACTION );
     54
     55                $this->assertGreaterThan( 0, has_action( 'customize_controls_enqueue_scripts', array( 'Widget_Customizer', 'customize_controls_enqueue_deps' ) ), 'customize_controls_enqueue_deps method is not properly invoked during customize_controls_enqueue_scripts' );
     56
     57                $this->assertGreaterThan( 0, has_action( 'customize_preview_init', array( 'Widget_Customizer', 'customize_preview_init' ) ), 'customize_preview_init method is not properly invoked during customize_preview_init' );
     58
     59                $this->assertGreaterThan( 0, has_action( 'widgets_admin_page', array( 'Widget_Customizer', 'widget_customizer_link' ) ), 'widget_customizer_link method is not properly invoked during widgets_admin_page' );
     60
     61                $this->assertGreaterThan( 0, has_action( 'dynamic_sidebar', array( 'Widget_Customizer', 'tally_sidebars_via_dynamic_sidebar_actions' ) ), 'tally_sidebars_via_dynamic_sidebar_actions method is not properly invoked during dynamic_sidebar' );
     62        }
     63
     64        public function test_setup_filters() {
     65                // Makes sure all registered filters are invoked in expected hooks
     66
     67                $this->assertEquals( 10, has_action( 'temp_is_active_sidebar', array( 'Widget_Customizer', 'tally_sidebars_via_is_active_sidebar_calls' ) ), 'tally_sidebars_via_is_active_sidebar_calls method is not properly invoked during temp_is_active_sidebar' );
     68
     69                $this->assertEquals( 10, has_action( 'temp_dynamic_sidebar_has_widgets', array( 'Widget_Customizer', 'tally_sidebars_via_dynamic_sidebar_calls' ) ), 'tally_sidebars_via_dynamic_sidebar_calls method is not properly invoked during temp_dynamic_sidebar_has_widgets' );
     70        }
     71
     72        public function test_plugin_meta() {
     73                $this->assertEquals( 'widget-customizer', Widget_Customizer::get_plugin_meta( 'TextDomain' ), 'Unexpected TextDomain value of plugin data' );
     74                $this->assertEquals( '/languages', Widget_Customizer::get_plugin_meta( 'DomainPath' ), 'Unexpected DomainPath value of plugin data' );
     75                $this->assertEquals( 'Widget Customizer', Widget_Customizer::get_plugin_meta( 'Name' ), 'Unexpected Version value plugin data' );
     76        }
     77
     78        public function test_preview_new_widgets() {
     79                // @todo Adds test here. Please note that this is the most tricky test as it tests AJAX request.
     80        }
     81
     82        public function test_customize_register() {
     83                // Since two classes are expected to be loaded in here, tests it if those classess exist.
     84                $expected_classes_loaded = (
     85                        class_exists( 'Sidebar_Widgets_WP_Customize_Control' )
     86                        &&
     87                        class_exists( 'Widget_Form_WP_Customize_Control' )
     88                );
     89
     90                $this->assertTrue( $expected_classes_loaded, 'Sidebar_Widgets_WP_Customize_Control and Widget_Form_WP_Customize_Control are not loaded properly' );
     91
     92                // @todo Puts more assertions
     93        }
     94
     95        public function test_customize_controls_enqueue_deps() {
     96                global $wp_scripts;
     97
     98                remove_all_actions( 'customize_controls_enqueue_scripts' );
     99                add_action( 'customize_controls_enqueue_scripts', array( 'Widget_Customizer', 'customize_controls_enqueue_deps' ) );
     100                do_action( 'customize_controls_enqueue_scripts' );
     101
     102                $this->assertTrue( wp_script_is( 'jquery-ui-sortable', 'enqueued' ), 'jquery-ui-sortable script is not properly enqueued' );
     103                $this->assertTrue( wp_script_is( 'jquery-ui-droppable', 'enqueued' ), 'jquery-ui-droppable script is not properly enqueued' );
     104                $this->assertTrue( wp_script_is( 'widget-customizer', 'enqueued' ), 'widget-customizer script is not properly enqueued' );
     105
     106                $this->assertTrue( wp_style_is( 'widget-customizer', 'enqueued' ), 'widget-customizer style is not properly enqueued' );
     107
     108                $this->assertNotEmpty( $wp_scripts->get_data( 'widget-customizer', 'data' ), 'widget-customizer data is empty' );
     109        }
     110}