Ticket #27112: 27112.diff
File 27112.diff, 156.1 KB (added by , 10 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 342 body.adding-widget .add-new-widget, 343 body.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 } 350 body.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 429 body.adding-widget #available-widgets { 430 left: 0; 431 } 432 433 body.adding-widget .wp-full-overlay-main { 434 left: 300px; 435 } 436 437 body.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 */ 3 var 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
113 113 114 114 // This theme uses its own gallery styles. 115 115 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' ); 116 120 } 117 121 endif; // twentyfourteen_setup 118 122 add_action( 'after_setup_theme', 'twentyfourteen_setup' ); -
src/wp-content/themes/twentyfourteen/inc/widgets.php
44 44 parent::__construct( 'widget_twentyfourteen_ephemera', __( 'Twenty Fourteen Ephemera', 'twentyfourteen' ), array( 45 45 'classname' => 'widget_twentyfourteen_ephemera', 46 46 'description' => __( 'Use this widget to list your recent Aside, Quote, Video, Audio, Image, Gallery, and Link posts', 'twentyfourteen' ), 47 'customizer_support' => true, 47 48 ) ); 48 49 49 50 /* -
src/wp-content/themes/twentyfourteen/js/customizer.js
35 35 } 36 36 } ); 37 37 } ); 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 } ); 38 46 } )( jQuery ); 47 No newline at end of file -
src/wp-content/themes/twentythirteen/functions.php
105 105 106 106 // This theme uses its own gallery styles. 107 107 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' ); 108 112 } 109 113 add_action( 'after_setup_theme', 'twentythirteen_setup' ); 110 114 -
src/wp-content/themes/twentythirteen/js/theme-customizer.js
37 37 } 38 38 } ); 39 39 } ); 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 } ); 40 48 } )( jQuery ); -
src/wp-includes/class-wp-customize-control.php
814 814 foreach ( $this->default_headers as $choice => $header ) 815 815 $this->print_header_image( $choice, $header ); 816 816 } 817 } 818 No newline at end of file 817 } 818 819 /** 820 * Widget Area Customize Control Class 821 * 822 */ 823 class 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 */ 852 class 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
31 31 require( ABSPATH . WPINC . '/class-wp-customize-setting.php' ); 32 32 require( ABSPATH . WPINC . '/class-wp-customize-section.php' ); 33 33 require( ABSPATH . WPINC . '/class-wp-customize-control.php' ); 34 require( ABSPATH . WPINC . '/class-wp-customize-widgets.php' ); 34 35 36 WP_Customize_Widgets::setup(); // This should be integrated. 37 35 38 add_filter( 'wp_die_handler', array( $this, 'wp_die_handler' ) ); 36 39 37 40 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 */ 5 class 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…' ) ?>"> 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 1381 class Widget_Customizer_Exception extends Exception {} 1382 1383 class 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 */ 3 var 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 2 class 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 }