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