Ticket #28709: 28709.wip.8.diff
File 28709.wip.8.diff, 64.1 KB (added by , 10 years ago) |
---|
-
src/wp-admin/customize.php
diff --git src/wp-admin/customize.php src/wp-admin/customize.php index ed525581..c4bfc8c 100644
do_action( 'customize_controls_init' ); 53 53 wp_enqueue_script( 'customize-controls' ); 54 54 wp_enqueue_style( 'customize-controls' ); 55 55 56 wp_enqueue_script( 'accordion' );57 58 56 /** 59 57 * Enqueue Customizer control scripts. 60 58 * … … do_action( 'customize_controls_print_scripts' ); 130 128 ?> 131 129 132 130 <div id="widgets-right"><!-- For Widget Customizer, many widgets try to look for instances under div#widgets-right, so we have to add that ID to a container div in the Customizer for compat --> 133 <div class="wp-full-overlay-sidebar-content accordion-container" tabindex="-1">131 <div class="wp-full-overlay-sidebar-content" tabindex="-1"> 134 132 <div id="customize-info" class="accordion-section <?php if ( $cannot_expand ) echo ' cannot-expand'; ?>"> 135 133 <div class="accordion-section-title" aria-label="<?php esc_attr_e( 'Customizer Options' ); ?>" tabindex="0"> 136 134 <span class="preview-notice"><?php … … do_action( 'customize_controls_print_scripts' ); 160 158 <?php endif; ?> 161 159 </div> 162 160 163 <div id="customize-theme-controls"><ul> 164 <?php 165 foreach ( $wp_customize->containers() as $container ) { 166 $container->maybe_render(); 167 } 168 ?> 169 </ul></div> 161 <div id="customize-theme-controls"> 162 <ul><?php // Panels and sections are managed here via JavaScript ?></ul> 163 </div> 170 164 </div> 171 165 </div> 172 166 … … do_action( 'customize_controls_print_scripts' ); 252 246 ), 253 247 'settings' => array(), 254 248 'controls' => array(), 249 'panels' => array(), 250 'sections' => array(), 255 251 'nonce' => array( 256 252 'save' => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ), 257 253 'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() ) … … do_action( 'customize_controls_print_scripts' ); 266 262 ); 267 263 } 268 264 269 // Prepare Customize Control objects to pass to Java script.265 // Prepare Customize Control objects to pass to JavaScript. 270 266 foreach ( $wp_customize->controls() as $id => $control ) { 271 $control->to_json(); 272 $settings['controls'][ $id ] = $control->json; 267 $settings['controls'][ $id ] = $control->json(); 268 } 269 270 // Prepare Customize Section objects to pass to JavaScript. 271 foreach ( $wp_customize->sections() as $id => $section ) { 272 $settings['sections'][ $id ] = $section->json(); 273 } 274 275 // Prepare Customize Panel objects to pass to JavaScript. 276 foreach ( $wp_customize->panels() as $id => $panel ) { 277 $settings['panels'][ $id ] = $panel->json(); 278 foreach ( $panel->sections as $section_id => $section ) { 279 $settings['sections'][ $section_id ] = $section->json(); 280 } 273 281 } 274 282 275 283 ?> -
src/wp-admin/js/accordion.js
diff --git src/wp-admin/js/accordion.js src/wp-admin/js/accordion.js index 6cb1c1c..1769d27 100644
25 25 * 26 26 * Note that any appropriate tags may be used, as long as the above classes are present. 27 27 * 28 * In addition to the standard accordion behavior, this file includes JS for the29 * Customizer's "Panel" functionality.30 *31 28 * @since 3.6.0. 32 29 */ 33 30 … … 46 43 accordionSwitch( $( this ) ); 47 44 }); 48 45 49 // Go back to the top-level Customizer accordion.50 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) {51 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key52 return;53 }54 55 e.preventDefault(); // Keep this AFTER the key filter above56 57 panelSwitch( $( '.current-panel' ) );58 });59 46 }); 60 47 61 var sectionContent = $( '.accordion-section-content' );62 63 48 /** 64 49 * Close the current accordion section and open a new one. 65 50 * … … 69 54 function accordionSwitch ( el ) { 70 55 var section = el.closest( '.accordion-section' ), 71 56 siblings = section.closest( '.accordion-container' ).find( '.open' ), 72 content = section.find( sectionContent);57 content = section.find( '.accordion-section-content' ); 73 58 74 59 // This section has no content and cannot be expanded. 75 60 if ( section.hasClass( 'cannot-expand' ) ) { 76 61 return; 77 62 } 78 63 79 // Slide into a sub-panel instead of accordioning (Customizer-specific).80 if ( section.hasClass( 'control-panel' ) ) {81 panelSwitch( section );82 return;83 }84 85 64 if ( section.hasClass( 'open' ) ) { 86 65 section.toggleClass( 'open' ); 87 66 content.toggle( true ).slideToggle( 150 ); 88 67 } else { 89 68 siblings.removeClass( 'open' ); 90 siblings.find( sectionContent).show().slideUp( 150 );69 siblings.find( '.accordion-section-content' ).show().slideUp( 150 ); 91 70 content.toggle( false ).slideToggle( 150 ); 92 71 section.toggleClass( 'open' ); 93 72 } 94 73 } 95 74 96 /**97 * Slide into an accordion sub-panel.98 *99 * For the Customizer-specific panel functionality100 *101 * @param {Object} panel Title element or back button of the accordion panel to toggle.102 * @since 4.0.0103 */104 function panelSwitch( panel ) {105 var position, scroll,106 section = panel.closest( '.accordion-section' ),107 overlay = section.closest( '.wp-full-overlay' ),108 container = section.closest( '.accordion-container' ),109 siblings = container.find( '.open' ),110 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ),111 backBtn = overlay.find( '.control-panel-back' ),112 panelTitle = section.find( '.accordion-section-title' ).first(),113 content = section.find( '.control-panel-content' );114 115 if ( section.hasClass( 'current-panel' ) ) {116 section.toggleClass( 'current-panel' );117 overlay.toggleClass( 'in-sub-panel' );118 content.delay( 180 ).hide( 0, function() {119 content.css( 'margin-top', 'inherit' ); // Reset120 } );121 topPanel.attr( 'tabindex', '0' );122 backBtn.attr( 'tabindex', '-1' );123 panelTitle.focus();124 container.scrollTop( 0 );125 } else {126 // Close all open sections in any accordion level.127 siblings.removeClass( 'open' );128 siblings.find( sectionContent ).show().slideUp( 0 );129 content.show( 0, function() {130 position = content.offset().top;131 scroll = container.scrollTop();132 content.css( 'margin-top', ( 45 - position - scroll ) );133 section.toggleClass( 'current-panel' );134 overlay.toggleClass( 'in-sub-panel' );135 container.scrollTop( 0 );136 } );137 topPanel.attr( 'tabindex', '-1' );138 backBtn.attr( 'tabindex', '0' );139 backBtn.focus();140 }141 }142 143 75 })(jQuery); -
src/wp-admin/js/customize-controls.js
diff --git src/wp-admin/js/customize-controls.js src/wp-admin/js/customize-controls.js index 812f4fa..5ed6add 100644
1 1 /* globals _wpCustomizeHeader, _wpMediaViewsL10n */ 2 2 (function( exports, $ ){ 3 var api = wp.customize;3 var bubbleChildValueChanges, Container, focus, api = wp.customize; 4 4 5 5 /** 6 6 * @constructor … … 31 31 }); 32 32 33 33 /** 34 * Watch all changes to Value properties, and bubble changes to parent Values instance 35 * 36 * @param {wp.customize.Class} instance 37 * @param {Array} properties The names of the Value instances to watch. 38 */ 39 bubbleChildValueChanges = function ( instance, properties ) { 40 $.each( properties, function ( i, key ) { 41 instance[ key ].bind( function ( to, from ) { 42 if ( instance.parent && to !== from ) { 43 instance.parent.trigger( 'change', instance ); 44 } 45 } ); 46 } ); 47 }; 48 49 /** 50 * Expand a panel, section, or control and focus on the first focusable element. 51 * 52 * @param {Object} [params] 53 */ 54 focus = function ( params ) { 55 var container, completeCallback, focus; 56 container = this; 57 params = params || {}; 58 focus = function () { 59 container.container.find( ':focusable:first' ).focus(); 60 }; 61 if ( params.completeCallback ) { 62 completeCallback = params.completeCallback; 63 params.completeCallback = function () { 64 focus(); 65 completeCallback(); 66 }; 67 } else { 68 params.completeCallback = focus; 69 } 70 if ( container.expand ) { 71 container.expand( params ); 72 } else { 73 params.completeCallback(); 74 } 75 }; 76 77 /** 78 * Base class for Panel and Section 79 * 34 80 * @constructor 35 81 * @augments wp.customize.Class 36 82 */ 37 api.Control = api.Class.extend({ 38 initialize: function( id, options ) { 39 var control = this, 40 nodes, radios, settings; 83 Container = api.Class.extend({ 84 defaultActiveArguments: { duration: null }, 85 defaultExpandedArguments: { duration: 150 }, 86 87 initialize: function ( id, options ) { 88 var container = this; 89 container.id = id; 90 container.params = {}; 91 $.extend( container, options || {} ); 92 container.container = $( container.params.content ); 93 94 container.priority = new api.Value(); 95 container.active = new api.Value(); 96 container.activeArgumentsQueue = []; 97 container.expanded = new api.Value(); 98 container.expandedArgumentsQueue = []; 99 100 container.active.bind( function ( active ) { 101 var args = container.activeArgumentsQueue.shift(); 102 args = $.extend( {}, container.defaultActiveArguments, args ); 103 active = ( active && container.isContextuallyActive() ); 104 container.onChangeActive( active, args ); 105 // @todo trigger 'activated' and 'deactivated' events based on the expanded param? 106 }); 107 container.expanded.bind( function ( expanded ) { 108 var args = container.expandedArgumentsQueue.shift(); 109 args = $.extend( {}, container.defaultExpandedArguments, args ); 110 container.onChangeExpanded( expanded, args ); 111 // @todo trigger 'expanded' and 'collapsed' events based on the expanded param? 112 }); 41 113 42 this.params = {}; 43 $.extend( this, options || {} ); 114 container.attachEvents(); 44 115 45 this.id = id; 46 this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); 47 this.container = $( this.selector ); 48 this.active = new api.Value( this.params.active ); 116 bubbleChildValueChanges( container, [ 'priority', 'active' ] ); 49 117 50 settings = $.map( this.params.settings, function( value ) { 51 return value; 118 container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority ); 119 container.active.set( container.params.active ); 120 container.expanded.set( false ); // @todo True if deeplinking? 121 }, 122 123 /** 124 * Get the child models associated with this parent, sorting them by their priority Value. 125 * 126 * @param {String} parentType 127 * @param {String} childType 128 * @returns {Array} 129 */ 130 _children: function ( parentType, childType ) { 131 var parent = this, 132 children = []; 133 api[ childType ].each( function ( child ) { 134 if ( child[ parentType ].get() === parent.id ) { 135 children.push( child ); 136 } 137 } ); 138 children.sort( function ( a, b ) { 139 return a.priority() - b.priority(); 140 } ); 141 return children; 142 }, 143 144 /** 145 * To override by subclass, to return whether the container has active children. 146 */ 147 isContextuallyActive: function () { 148 throw new Error( 'Must override with subclass.' ); 149 }, 150 151 /** 152 * Handle changes to the active state. 153 * This does not change the active state, it merely handles the behavior 154 * for when it does change. 155 * 156 * To override by subclass, update the container's UI to reflect the provided active state. 157 * 158 * @param {Boolean} active 159 * @param {Object} args merged on top of this.defaultActiveArguments 160 */ 161 onChangeActive: function ( active, args ) { 162 if ( active ) { 163 this.container.stop( true, true ).slideDown( args.duration ); // @todo pass args.completeCallback 164 } else { 165 this.container.stop( true, true ).slideUp( args.duration ); // @todo pass args.completeCallback 166 } 167 }, 168 169 /** 170 * @params {Boolean} active 171 * @param {Object} [params] 172 * @returns {Boolean} false if state already applied 173 */ 174 _toggleActive: function ( active, params ) { 175 var self = this; 176 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) { 177 setTimeout( function () { 178 self.onChangeActive( self.active.get(), params || {} ); 179 }); 180 return false; 181 } 182 this.activeArgumentsQueue.push( params || {} ); 183 this.active.set( active ); 184 return true; 185 }, 186 187 /** 188 * @param {Object} [params] 189 * @returns {Boolean} false if already active 190 */ 191 activate: function ( params ) { 192 return this._toggleActive( true, params ); 193 }, 194 195 /** 196 * @param {Object} [params] 197 * @returns {Boolean} false if already inactive 198 */ 199 deactivate: function ( params ) { 200 return this._toggleActive( false, params ); 201 }, 202 203 /** 204 * To override by subclass, update the container's UI to reflect the provided active state. 205 */ 206 onChangeExpanded: function () { 207 throw new Error( 'Must override with subclass.' ); 208 }, 209 210 /** 211 * @param {Boolean} expanded 212 * @param {Object} [params] 213 * @returns {Boolean} false if state already applied 214 */ 215 _toggleExpanded: function ( expanded, params ) { 216 var self = this; 217 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) { 218 setTimeout( function () { 219 self.onChangeExpanded( self.expanded.get(), params || {} ); 220 }); 221 return false; 222 } 223 this.expandedArgumentsQueue.push( params || {} ); 224 this.expanded.set( expanded ); 225 return true; 226 }, 227 228 /** 229 * @param {Object} [params] 230 * @returns {Boolean} false if already expanded 231 */ 232 expand: function ( params ) { 233 return this._toggleExpanded( true, params ); 234 }, 235 236 /** 237 * @param {Object} [params] 238 * @returns {Boolean} false if already collapsed 239 */ 240 collapse: function ( params ) { 241 return this._toggleExpanded( false, params ); 242 }, 243 244 /** 245 * Bring the container into view and then expand this and bring it into view 246 * @param {Object} [params] 247 */ 248 focus: focus 249 }); 250 251 /** 252 * @constructor 253 * @augments wp.customize.Class 254 */ 255 api.Section = Container.extend({ 256 257 /** 258 * @param {String} id 259 * @param {Array} options 260 */ 261 initialize: function ( id, options ) { 262 var section = this; 263 Container.prototype.initialize.call( section, id, options ); 264 265 section.panel = new api.Value(); 266 section.panel.bind( function ( id ) { 267 $( section.container ).toggleClass( 'control-subsection', !! id ); 52 268 }); 269 section.panel.set( section.params.panel || '' ); 270 bubbleChildValueChanges( section, [ 'panel' ] ); 271 }, 53 272 54 api.apply( api, settings.concat( function() { 55 var key; 273 /** 274 * 275 */ 276 embed: function ( readyCallback ) { 277 var panel_id, 278 section = this; 279 280 panel_id = this.panel.get(); 281 if ( ! panel_id ) { 282 $( '#customize-theme-controls > ul' ).append( section.container ); 283 readyCallback(); 284 } else { 285 api.panel( panel_id, function ( panel ) { 286 panel.embed(); 287 panel.container.find( 'ul:first' ).append( section.container ); 288 readyCallback(); 289 } ); 290 } 291 }, 56 292 57 control.settings = {}; 58 for ( key in control.params.settings ) { 59 control.settings[ key ] = api( control.params.settings[ key ] ); 293 /** 294 * Add behaviors for the accordion section 295 */ 296 attachEvents: function () { 297 var section = this; 298 299 // Expand/Collapse accordion sections on click. 300 section.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) { 301 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 302 return; 60 303 } 304 e.preventDefault(); // Keep this AFTER the key filter above 61 305 62 control.setting = control.settings['default'] || null; 63 control.renderContent( function() { 64 // Don't call ready() until the content has rendered. 65 control.ready(); 306 if ( section.expanded() ) { 307 section.collapse(); 308 } else { 309 section.expand(); 310 } 311 }); 312 }, 313 314 /** 315 * Return whether this section has any active controls. 316 * 317 * @returns {boolean} 318 */ 319 isContextuallyActive: function () { 320 var section = this, 321 controls = section.controls(), 322 activeCount = 0; 323 _( controls ).each( function ( control ) { 324 if ( control.active() ) { 325 activeCount += 1; 326 } 327 } ); 328 return ( activeCount !== 0 ); 329 }, 330 331 /** 332 * Get the controls that are associated with this section, sorted by their priority Value. 333 * 334 * @returns {Array} 335 */ 336 controls: function () { 337 return this._children( 'section', 'control' ); 338 }, 339 340 /** 341 * Update UI to reflect expanded state 342 * 343 * @param {Boolean} expanded 344 * @param {Object} args 345 */ 346 onChangeExpanded: function ( expanded, args ) { 347 var section = this, 348 content = section.container.find( '.accordion-section-content' ), 349 expand; 350 351 if ( expanded ) { 352 353 expand = function () { 354 content.stop().slideDown( args.duration, args.completeCallback ); 355 section.container.addClass( 'open' ); 356 }; 357 358 if ( ! args.allowMultiple ) { 359 api.section.each( function ( otherSection ) { 360 if ( otherSection !== section ) { 361 otherSection.collapse( {duration: 0} ); 362 } 363 }); 364 } 365 366 if ( section.panel() ) { 367 api.panel( section.panel() ).expand({ 368 duration: args.duration, 369 completeCallback: expand 370 }); 371 } else { 372 expand(); 373 } 374 375 } else { 376 section.container.removeClass( 'open' ); 377 content.slideUp( args.duration, args.completeCallback ); 378 } 379 } 380 }); 381 382 /** 383 * @constructor 384 * @augments wp.customize.Class 385 */ 386 api.Panel = Container.extend({ 387 initialize: function ( id, options ) { 388 var panel = this; 389 Container.prototype.initialize.call( panel, id, options ); 390 }, 391 392 /** 393 * 394 */ 395 embed: function ( readyCallback ) { 396 $( '#customize-theme-controls > ul' ).append( this.container ); 397 if ( readyCallback ) { 398 readyCallback(); 399 } 400 }, 401 402 /** 403 * 404 */ 405 attachEvents: function () { 406 var meta, panel = this; 407 408 // Expand/Collapse accordion sections on click. 409 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) { 410 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 411 return; 412 } 413 e.preventDefault(); // Keep this AFTER the key filter above 414 415 if ( ! panel.expanded() ) { 416 panel.expand(); 417 } 418 }); 419 420 meta = panel.container.find( '.panel-meta:first' ); 421 422 meta.find( '> .accordion-section-title' ).on( 'click keydown', function( e ) { 423 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 424 return; 425 } 426 e.preventDefault(); // Keep this AFTER the key filter above 427 428 if ( meta.hasClass( 'cannot-expand' ) ) { 429 return; 430 } 431 432 var content = meta.find( '.accordion-section-content:first' ); 433 if ( meta.hasClass( 'open' ) ) { 434 meta.toggleClass( 'open' ); 435 content.slideUp( 150 ); 436 } else { 437 content.slideDown( 150 ); 438 meta.toggleClass( 'open' ); 439 } 440 }); 441 442 }, 443 444 /** 445 * Get the sections that are associated with this panel, sorted by their priority Value. 446 * 447 * @returns {Array} 448 */ 449 sections: function () { 450 return this._children( 'panel', 'section' ); 451 }, 452 453 /** 454 * Return whether this panel has any active sections. 455 * 456 * @returns {boolean} 457 */ 458 isContextuallyActive: function () { 459 var panel = this, 460 sections = panel.sections(), 461 activeCount = 0; 462 _( sections ).each( function ( section ) { 463 if ( section.active() && section.isContextuallyActive() ) { 464 activeCount += 1; 465 } 466 } ); 467 return ( activeCount !== 0 ); 468 }, 469 470 /** 471 * Update UI to reflect expanded state 472 * 473 * @param {Boolean} expanded 474 * @param {Object} args merged with this.defaultExpandedArguments 475 */ 476 onChangeExpanded: function ( expanded, args ) { 477 478 // Note: there is a second argument 'args' passed 479 var position, scroll, 480 panel = this, 481 section = panel.container.closest( '.accordion-section' ), 482 overlay = section.closest( '.wp-full-overlay' ), 483 container = section.closest( '.accordion-container' ), 484 siblings = container.find( '.open' ), 485 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), 486 backBtn = overlay.find( '.control-panel-back' ), 487 panelTitle = section.find( '.accordion-section-title' ).first(), 488 content = section.find( '.control-panel-content' ); 489 490 if ( expanded ) { 491 492 // Collapse any sibling sections/panels 493 api.section.each( function ( section ) { 494 if ( ! section.panel() ) { 495 section.collapse( { duration: 0 } ); 496 } 497 }); 498 api.panel.each( function ( otherPanel ) { 499 if ( panel !== otherPanel ) { 500 otherPanel.collapse( { duration: 0 } ); 501 } 502 }); 503 504 content.show( 0, function() { 505 position = content.offset().top; 506 scroll = container.scrollTop(); 507 content.css( 'margin-top', ( 45 - position - scroll ) ); 508 section.addClass( 'current-panel' ); 509 overlay.addClass( 'in-sub-panel' ); 510 container.scrollTop( 0 ); 511 if ( args.completeCallback ) { 512 args.completeCallback(); 513 } 66 514 } ); 67 }) ); 515 topPanel.attr( 'tabindex', '-1' ); 516 backBtn.attr( 'tabindex', '0' ); 517 backBtn.focus(); 518 } else { 519 siblings.removeClass( 'open' ); 520 section.removeClass( 'current-panel' ); 521 overlay.removeClass( 'in-sub-panel' ); 522 content.delay( 180 ).hide( 0, function() { 523 content.css( 'margin-top', 'inherit' ); // Reset 524 if ( args.completeCallback ) { 525 args.completeCallback(); 526 } 527 } ); 528 topPanel.attr( 'tabindex', '0' ); 529 backBtn.attr( 'tabindex', '-1' ); 530 panelTitle.focus(); 531 container.scrollTop( 0 ); 532 } 533 } 534 }); 535 536 /** 537 * @constructor 538 * @augments wp.customize.Class 539 */ 540 api.Control = api.Class.extend({ 541 defaultActiveArguments: { duration: null }, 542 543 initialize: function( id, options ) { 544 var control = this, 545 nodes, radios, settings; 546 547 control.params = {}; 548 $.extend( control, options || {} ); 549 550 control.id = id; 551 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); 552 control.container = control.params.content ? $( control.params.content ) : $( control.selector ); 553 554 control.section = new api.Value(); 555 control.priority = new api.Value(); 556 control.active = new api.Value(); 557 control.activeArgumentsQueue = []; 68 558 69 559 control.elements = []; 70 560 71 nodes = this.container.find('[data-customize-setting-link]');561 nodes = control.container.find('[data-customize-setting-link]'); 72 562 radios = {}; 73 563 74 564 nodes.each( function() { 75 var node = $( this),565 var node = $( this ), 76 566 name; 77 567 78 if ( node.is( ':radio') ) {79 name = node.prop( 'name');80 if ( radios[ name ] ) 568 if ( node.is( ':radio' ) ) { 569 name = node.prop( 'name' ); 570 if ( radios[ name ] ) { 81 571 return; 572 } 82 573 83 574 radios[ name ] = true; 84 575 node = nodes.filter( '[name="' + name + '"]' ); 85 576 } 86 577 87 api( node.data( 'customizeSettingLink'), function( setting ) {578 api( node.data( 'customizeSettingLink' ), function( setting ) { 88 579 var element = new api.Element( node ); 89 580 control.elements.push( element ); 90 581 element.sync( setting ); … … 93 584 }); 94 585 95 586 control.active.bind( function ( active ) { 96 control.toggle( active ); 587 var args = control.activeArgumentsQueue.shift(); 588 args = $.extend( {}, control.defaultActiveArguments, args ); 589 control.onChangeActive( active, args ); 590 } ); 591 592 control.section.set( control.params.section ); 593 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority ); 594 control.active.set( control.params.active ); 595 596 bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] ); 597 598 // Associate this control with its settings when they are created 599 settings = $.map( control.params.settings, function( value ) { 600 return value; 601 }); 602 api.apply( api, settings.concat( function () { 603 var key; 604 605 control.settings = {}; 606 for ( key in control.params.settings ) { 607 control.settings[ key ] = api( control.params.settings[ key ] ); 608 } 609 610 control.setting = control.settings['default'] || null; 611 control.embed( function () { 612 control.renderContent( function() { 613 // Don't call ready() until the content has rendered. 614 control.ready(); 615 }); 616 }); 617 }) ); 618 }, 619 620 /** 621 * @param {Function} [readyCallback] Callback to fire when the embedding is done. 622 */ 623 embed: function ( readyCallback ) { 624 var section_id, 625 control = this; 626 627 section_id = control.section.get(); 628 if ( ! section_id ) { 629 throw new Error( 'A control must have an associated section.' ); 630 } 631 632 // Defer until the associated section is available 633 api.section( section_id, function ( section ) { 634 section.embed( function () { 635 section.container.find( 'ul:first' ).append( control.container ); 636 readyCallback(); 637 } ); 97 638 } ); 98 control.toggle( control.active() );99 639 }, 100 640 101 641 /** … … 104 644 ready: function() {}, 105 645 106 646 /** 107 * Callback for change to the control's active state. 108 * 109 * Override function for custom behavior for the control being active/inactive. 647 * Bring the containing section and panel into view and then this control into view, focusing on the first input 648 */ 649 focus: focus, 650 651 /** 652 * Update UI in response to a change in the control's active state. 653 * This does not change the active state, it merely handles the behavior 654 * for when it does change. 110 655 * 111 656 * @param {Boolean} active 657 * @param {Object} args merged on top of this.defaultActiveArguments 112 658 */ 113 toggle: function ( active) {659 onChangeActive: function ( active, args ) { 114 660 if ( active ) { 115 this.container.slideDown( );661 this.container.slideDown( args.duration ); 116 662 } else { 117 this.container.slideUp( );663 this.container.slideUp( args.duration ); 118 664 } 119 665 }, 120 666 667 /** 668 * @deprecated alias of onChangeActive 669 */ 670 toggle: function ( active ) { 671 return this.onChangeActive( active, this.defaultActiveArguments ); 672 }, 673 674 /** 675 * Shorthand way to enable the active state. 676 * 677 * @param {Object} [params] 678 * @returns {Boolean} false if already active 679 */ 680 activate: Container.prototype.activate, 681 682 /** 683 * Shorthand way to disable the active state. 684 * 685 * @param {Object} [params] 686 * @returns {Boolean} false if already inactive 687 */ 688 deactivate: Container.prototype.deactivate, 689 121 690 dropdownInit: function() { 122 691 var control = this, 123 692 statuses = this.container.find('.dropdown-status'), … … 158 727 * Render the control from its JS template, if it exists. 159 728 * 160 729 * The control's container must alreasy exist in the DOM. 730 * 731 * @param {Function} [callback] 161 732 */ 162 733 renderContent: function( callback ) { 163 734 var template, 164 selector = 'customize-control-' + this.params.type + '-content', 165 callback = callback || function(){}; 735 selector = 'customize-control-' + this.params.type + '-content'; 166 736 if ( 0 !== $( '#tmpl-' + selector ).length ) { 167 737 template = wp.template( selector ); 168 738 if ( template && this.container ) { 169 739 this.container.append( template( this.params ) ); 170 740 } 171 741 } 172 callback(); 742 if ( callback ) { 743 callback(); 744 } 173 745 } 174 746 }); 175 747 … … 596 1168 597 1169 // Create the collection of Control objects. 598 1170 api.control = new api.Values({ defaultConstructor: api.Control }); 1171 api.section = new api.Values({ defaultConstructor: api.Section }); 1172 api.panel = new api.Values({ defaultConstructor: api.Panel }); 599 1173 600 1174 /** 601 1175 * @constructor … … 631 1205 loaded = false, 632 1206 ready = false; 633 1207 634 if ( this._ready ) 1208 if ( this._ready ) { 635 1209 this.unbind( 'ready', this._ready ); 1210 } 636 1211 637 1212 this._ready = function() { 638 1213 ready = true; 639 1214 640 if ( loaded ) 1215 if ( loaded ) { 641 1216 deferred.resolveWith( self ); 1217 } 642 1218 }; 643 1219 644 1220 this.bind( 'ready', this._ready ); 645 1221 646 1222 this.bind( 'ready', function ( data ) { 647 if ( ! data || ! data.activeControls) {1223 if ( ! data ) { 648 1224 return; 649 1225 } 650 1226 651 $.each( data.activeControls, function ( id, active ) { 652 var control = api.control( id ); 653 if ( control ) { 654 control.active( active ); 1227 var constructs = { 1228 panel: data.activePanels, 1229 section: data.activeSections, 1230 control: data.activeControls 1231 }; 1232 1233 $.each( constructs, function ( type, activeConstructs ) { 1234 if ( activeConstructs ) { 1235 $.each( activeConstructs, function ( id, active ) { 1236 var construct = api[ type ]( id ); 1237 if ( construct ) { 1238 construct.active( active ); 1239 } 1240 } ); 655 1241 } 656 1242 } ); 1243 657 1244 } ); 658 1245 659 1246 this.request = $.ajax( this.previewUrl(), { … … 675 1262 676 1263 // Check if the location response header differs from the current URL. 677 1264 // If so, the request was redirected; try loading the requested page. 678 if ( location && location != self.previewUrl() ) {1265 if ( location && location !== self.previewUrl() ) { 679 1266 deferred.rejectWith( self, [ 'redirect', location ] ); 680 1267 return; 681 1268 } … … 1000 1587 image: api.ImageControl, 1001 1588 header: api.HeaderControl 1002 1589 }; 1590 api.panelConstructor = {}; 1591 api.sectionConstructor = {}; 1003 1592 1004 1593 $( function() { 1005 1594 api.settings = window._wpCustomizeSettings; … … 1030 1619 } 1031 1620 }); 1032 1621 1622 // Expand/Collapse the main customizer customize info 1623 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( e ) { 1624 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 1625 return; 1626 } 1627 e.preventDefault(); // Keep this AFTER the key filter above 1628 1629 var section = $( this ).parent(), 1630 content = section.find( '.accordion-section-content:first' ); 1631 1632 if ( section.hasClass( 'cannot-expand' ) ) { 1633 return; 1634 } 1635 1636 if ( section.hasClass( 'open' ) ) { 1637 section.toggleClass( 'open' ); 1638 content.slideUp( 150 ); 1639 } else { 1640 content.slideDown( 150 ); 1641 section.toggleClass( 'open' ); 1642 } 1643 }); 1644 1033 1645 // Initialize Previewer 1034 1646 api.previewer = new api.Previewer({ 1035 1647 container: '#customize-preview', … … 1123 1735 $.extend( this.nonce, nonce ); 1124 1736 }); 1125 1737 1738 // Create Settings 1126 1739 $.each( api.settings.settings, function( id, data ) { 1127 1740 api.create( id, id, data.value, { 1128 1741 transport: data.transport, … … 1130 1743 } ); 1131 1744 }); 1132 1745 1746 // Create Panels 1747 $.each( api.settings.panels, function ( id, data ) { 1748 var constructor = api.panelConstructor[ data.type ] || api.Panel, 1749 panel; 1750 1751 panel = new constructor( id, { 1752 params: data 1753 } ); 1754 api.panel.add( id, panel ); 1755 }); 1756 1757 // Create Sections 1758 $.each( api.settings.sections, function ( id, data ) { 1759 var constructor = api.sectionConstructor[ data.type ] || api.Section, 1760 section; 1761 1762 section = new constructor( id, { 1763 params: data 1764 } ); 1765 api.section.add( id, section ); 1766 }); 1767 1768 // Create Controls 1769 // @todo factor this out 1133 1770 $.each( api.settings.controls, function( id, data ) { 1134 1771 var constructor = api.controlConstructor[ data.type ] || api.Control, 1135 1772 control; 1136 1773 1137 control = api.control.add( id,new constructor( id, {1774 control = new constructor( id, { 1138 1775 params: data, 1139 1776 previewer: api.previewer 1140 } ) ); 1777 } ); 1778 api.control.add( id, control ); 1141 1779 }); 1142 1780 1781 /** 1782 * Sort panels, sections, controls by priorities. Hide empty sections and panels. 1783 */ 1784 api.reflowPaneContents = _.bind( function () { 1785 1786 var appendContainer, activeElement, rootNodes = []; 1787 1788 if ( document.activeElement ) { 1789 activeElement = $( document.activeElement ); 1790 } 1791 1792 api.panel.each( function ( panel ) { 1793 var sections = panel.sections(); 1794 rootNodes.push( panel ); 1795 appendContainer = panel.container.find( 'ul:first' ); 1796 // @todo Skip doing any DOM manipulation if the ordering is already correct 1797 _( sections ).each( function ( section ) { 1798 appendContainer.append( section.container ); 1799 } ); 1800 } ); 1801 1802 api.section.each( function ( section ) { 1803 var controls = section.controls(); 1804 if ( ! section.panel() ) { 1805 rootNodes.push( section ); 1806 } 1807 appendContainer = section.container.find( 'ul:first' ); 1808 // @todo Skip doing any DOM manipulation if the ordering is already correct 1809 _( controls ).each( function ( control ) { 1810 appendContainer.append( control.container ); 1811 } ); 1812 } ); 1813 1814 // Sort the root elements 1815 rootNodes.sort( function ( a, b ) { 1816 return a.priority() - b.priority(); 1817 } ); 1818 appendContainer = $( '#customize-theme-controls > ul' ); 1819 // @todo Skip doing any DOM manipulation if the ordering is already correct 1820 _( rootNodes ).each( function ( rootNode ) { 1821 appendContainer.append( rootNode.container ); 1822 } ); 1823 1824 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered 1825 api.panel.each( function ( panel ) { 1826 var value = panel.active(); 1827 panel.active.callbacks.fireWith( panel.active, [ value, value ] ); 1828 } ); 1829 api.section.each( function ( section ) { 1830 var value = section.active(); 1831 section.active.callbacks.fireWith( section.active, [ value, value ] ); 1832 } ); 1833 1834 if ( activeElement ) { 1835 activeElement.focus(); 1836 } 1837 }, api ); 1838 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 ); 1839 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { 1840 values.bind( 'add', api.reflowPaneContents ); 1841 values.bind( 'change', api.reflowPaneContents ); 1842 values.bind( 'remove', api.reflowPaneContents ); 1843 } ); 1844 api.bind( 'ready', api.reflowPaneContents ); 1845 1143 1846 // Check if preview url is valid and load the preview frame. 1144 1847 if ( api.previewer.previewUrl() ) { 1145 1848 api.previewer.refresh(); … … 1204 1907 event.preventDefault(); 1205 1908 }); 1206 1909 1910 // Go back to the top-level Customizer accordion. 1911 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) { 1912 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 1913 return; 1914 } 1915 1916 e.preventDefault(); // Keep this AFTER the key filter above 1917 api.panel.each( function ( panel ) { 1918 panel.collapse(); 1919 }); 1920 }); 1921 1207 1922 closeBtn.keydown( function( event ) { 1208 1923 if ( 9 === event.which ) // tab 1209 1924 return; -
src/wp-admin/js/customize-widgets.js
diff --git src/wp-admin/js/customize-widgets.js src/wp-admin/js/customize-widgets.js index 6be9a08..b9d8bd1 100644
404 404 * @augments wp.customize.Control 405 405 */ 406 406 api.Widgets.WidgetControl = api.Control.extend({ 407 defaultExpandedArguments: { 408 duration: 'fast' 409 }, 410 411 initialize: function ( id, options ) { 412 var control = this; 413 api.Control.prototype.initialize.call( control, id, options ); 414 control.expanded = new api.Value(); 415 control.expandedArgumentsQueue = []; 416 control.expanded.bind( function ( expanded ) { 417 var args = control.expandedArgumentsQueue.shift(); 418 args = $.extend( {}, control.defaultExpandedArguments, args ); 419 control.onChangeExpanded( expanded, args ); 420 }); 421 control.expanded.set( false ); 422 }, 423 407 424 /** 408 425 * Set up the control 409 426 */ … … 529 546 if ( sidebarWidgetsControl.isReordering ) { 530 547 return; 531 548 } 532 self. toggleForm();549 self.expanded( ! self.expanded() ); 533 550 } ); 534 551 535 552 $closeBtn = this.container.find( '.widget-control-close' ); 536 553 $closeBtn.on( 'click', function( e ) { 537 554 e.preventDefault(); 538 self.collapse Form();555 self.collapse(); 539 556 self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility 540 557 } ); 541 558 }, … … 778 795 * 779 796 * @param {Boolean} active 780 797 */ 781 toggle: function ( active ) { 798 onChangeActive: function ( active ) { 799 // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments 782 800 this.container.toggleClass( 'widget-rendered', active ); 783 801 }, 784 802 … … 1101 1119 * Expand the accordion section containing a control 1102 1120 */ 1103 1121 expandControlSection: function() { 1104 var $section = this.container.closest( '.accordion-section' ); 1105 1106 if ( ! $section.hasClass( 'open' ) ) { 1107 $section.find( '.accordion-section-title:first' ).trigger( 'click' ); 1108 } 1122 api.section( this.section() ).expand(); 1109 1123 }, 1110 1124 1111 1125 /** 1126 * @param {Boolean} expanded 1127 * @param {Object} [params] 1128 * @returns {Boolean} false if state already applied 1129 */ 1130 _toggleExpanded: api.Section.prototype._toggleExpanded, 1131 1132 /** 1133 * @param {Object} [params] 1134 * @returns {Boolean} false if already expanded 1135 */ 1136 expand: api.Section.prototype.expand, 1137 1138 /** 1112 1139 * Expand the widget form control 1140 * 1141 * @deprecated alias of expand() 1113 1142 */ 1114 1143 expandForm: function() { 1115 this. toggleForm( true);1144 this.expand(); 1116 1145 }, 1117 1146 1118 1147 /** 1148 * @param {Object} [params] 1149 * @returns {Boolean} false if already collapsed 1150 */ 1151 collapse: api.Section.prototype.collapse, 1152 1153 /** 1119 1154 * Collapse the widget form control 1155 * 1156 * @deprecated alias of expand() 1120 1157 */ 1121 1158 collapseForm: function() { 1122 this. toggleForm( false);1159 this.collapse(); 1123 1160 }, 1124 1161 1125 1162 /** 1126 1163 * Expand or collapse the widget control 1127 1164 * 1165 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) 1166 * 1128 1167 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility 1129 1168 */ 1130 1169 toggleForm: function( showOrHide ) { 1131 var self = this, $widget, $inside, complete; 1170 if ( typeof showOrHide === 'undefined' ) { 1171 showOrHide = ! this.expanded(); 1172 } 1173 this.expanded( showOrHide ); 1174 }, 1132 1175 1176 /** 1177 * Respond to change in the expanded state. 1178 * 1179 * @param {Boolean} expanded 1180 * @param {Object} args merged on top of this.defaultActiveArguments 1181 */ 1182 onChangeExpanded: function ( expanded, args ) { 1183 1184 var self = this, $widget, $inside, complete, prevComplete; 1133 1185 $widget = this.container.find( 'div.widget:first' ); 1134 1186 $inside = $widget.find( '.widget-inside:first' ); 1135 if ( typeof showOrHide === 'undefined' ) {1136 showOrHide = ! $inside.is( ':visible' );1137 }1138 1187 1139 // Already expanded or collapsed, so noop 1140 if ( $inside.is( ':visible' ) === showOrHide ) { 1141 return; 1142 } 1188 if ( expanded ) { 1189 1190 self.expandControlSection(); 1143 1191 1144 if ( showOrHide ) {1145 1192 // Close all other widget controls before expanding this one 1146 1193 api.control.each( function( otherControl ) { 1147 1194 if ( self.params.type === otherControl.params.type && self !== otherControl ) { 1148 otherControl.collapse Form();1195 otherControl.collapse(); 1149 1196 } 1150 1197 } ); 1151 1198 … … 1154 1201 self.container.addClass( 'expanded' ); 1155 1202 self.container.trigger( 'expanded' ); 1156 1203 }; 1204 if ( args.completeCallback ) { 1205 prevComplete = complete; 1206 complete = function () { 1207 prevComplete(); 1208 args.completeCallback(); 1209 }; 1210 } 1157 1211 1158 1212 if ( self.params.is_wide ) { 1159 $inside.fadeIn( 'fast', complete );1213 $inside.fadeIn( args.duration, complete ); 1160 1214 } else { 1161 $inside.slideDown( 'fast', complete );1215 $inside.slideDown( args.duration, complete ); 1162 1216 } 1163 1217 1164 1218 self.container.trigger( 'expand' ); 1165 1219 self.container.addClass( 'expanding' ); 1166 1220 } else { 1221 1167 1222 complete = function() { 1168 1223 self.container.removeClass( 'collapsing' ); 1169 1224 self.container.removeClass( 'expanded' ); 1170 1225 self.container.trigger( 'collapsed' ); 1171 1226 }; 1227 if ( args.completeCallback ) { 1228 prevComplete = complete; 1229 complete = function () { 1230 prevComplete(); 1231 args.completeCallback(); 1232 }; 1233 } 1172 1234 1173 1235 self.container.trigger( 'collapse' ); 1174 1236 self.container.addClass( 'collapsing' ); 1175 1237 1176 1238 if ( self.params.is_wide ) { 1177 $inside.fadeOut( 'fast', complete );1239 $inside.fadeOut( args.duration, complete ); 1178 1240 } else { 1179 $inside.slideUp( 'fast', function() {1241 $inside.slideUp( args.duration, function() { 1180 1242 $widget.css( { width:'', margin:'' } ); 1181 1243 complete(); 1182 1244 } ); … … 1185 1247 }, 1186 1248 1187 1249 /** 1188 * Expand the containing sidebar section, expand the form, and focus on1189 * the first input in the control1190 */1191 focus: function() {1192 this.expandControlSection();1193 this.expandForm();1194 this.container.find( '.widget-content :focusable:first' ).focus();1195 },1196 1197 /**1198 1250 * Get the position (index) of the widget in the containing sidebar 1199 1251 * 1200 1252 * @returns {Number} … … 1304 1356 * @augments wp.customize.Control 1305 1357 */ 1306 1358 api.Widgets.SidebarControl = api.Control.extend({ 1359 1307 1360 /** 1308 1361 * Set up the control 1309 1362 */ … … 1325 1378 registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id ); 1326 1379 1327 1380 this.setting.bind( function( newWidgetIds, oldWidgetIds ) { 1328 var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;1381 var widgetFormControls, removedWidgetIds, priority; 1329 1382 1330 1383 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); 1331 1384 … … 1350 1403 widgetFormControls.sort( function( a, b ) { 1351 1404 var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), 1352 1405 bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); 1406 return aIndex - bIndex; 1407 }); 1353 1408 1354 if ( aIndex === bIndex ) { 1355 return 0; 1356 } 1357 1358 return aIndex < bIndex ? -1 : 1; 1359 } ); 1360 1361 // Append the controls to put them in the right order 1362 finalControlContainers = _( widgetFormControls ).map( function( widgetFormControls ) { 1363 return widgetFormControls.container[0]; 1364 } ); 1365 1366 $sidebarWidgetsAddControl = self.$sectionContent.find( '.customize-control-sidebar_widgets' ); 1367 $sidebarWidgetsAddControl.before( finalControlContainers ); 1409 priority = 0; 1410 _( widgetFormControls ).each( function ( control ) { 1411 control.priority( priority ); 1412 control.section( self.section() ); 1413 priority += 1; 1414 }); 1415 self.priority( priority ); // Make sure sidebar control remains at end 1368 1416 1369 1417 // Re-sort widget form controls (including widgets form other sidebars newly moved here) 1370 1418 self._applyCardinalOrderClassNames(); … … 1434 1482 // Update the model with whether or not the sidebar is rendered 1435 1483 self.active.bind( function ( active ) { 1436 1484 registeredSidebar.set( 'is_rendered', active ); 1485 api.section( self.section.get() ).active( active ); 1437 1486 } ); 1438 }, 1439 1440 /** 1441 * Show the sidebar section when it becomes visible. 1442 * 1443 * Overrides api.Control.toggle() 1444 * 1445 * @param {Boolean} active 1446 */ 1447 toggle: function ( active ) { 1448 var $section, sectionSelector; 1449 1450 sectionSelector = '#accordion-section-sidebar-widgets-' + this.params.sidebar_id; 1451 $section = $( sectionSelector ); 1452 1453 if ( active ) { 1454 $section.stop().slideDown( function() { 1455 $( this ).css( 'height', 'auto' ); // so that the .accordion-section-content won't overflow 1456 } ); 1457 1458 } else { 1459 // Make sure that hidden sections get closed first 1460 if ( $section.hasClass( 'open' ) ) { 1461 // it would be nice if accordionSwitch() in accordion.js was public 1462 $section.find( '.accordion-section-title' ).trigger( 'click' ); 1463 } 1464 1465 $section.stop().slideUp(); 1466 } 1487 api.section( self.section.get() ).active( self.active() ); 1467 1488 }, 1468 1489 1469 1490 /** … … 1500 1521 this.$controlSection.find( '.accordion-section-title' ).droppable({ 1501 1522 accept: '.customize-control-widget_form', 1502 1523 over: function() { 1503 if ( ! self.$controlSection.hasClass( 'open' ) ) { 1504 self.$controlSection.addClass( 'open' ); 1505 self.$sectionContent.toggle( false ).slideToggle( 150, function() { 1506 self.$sectionContent.sortable( 'refreshPositions' ); 1507 } ); 1508 } 1524 var section = api.section( self.section.get() ); 1525 section.expand({ 1526 allowMultiple: true, // Prevent the section being dragged from to be collapsed 1527 completeCallback: function () { 1528 // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed 1529 api.section.each( function ( otherSection ) { 1530 if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) { 1531 otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' ); 1532 } 1533 } ); 1534 } 1535 }); 1509 1536 } 1510 1537 }); 1511 1538 … … 1548 1575 * Add classes to the widget_form controls to assist with styling 1549 1576 */ 1550 1577 _applyCardinalOrderClassNames: function() { 1551 this.$sectionContent.find( '.customize-control-widget_form' ) 1552 .removeClass( 'first-widget' ) 1553 .removeClass( 'last-widget' ) 1554 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); 1578 var widgetControls = []; 1579 _.each( this.setting(), function ( widgetId ) { 1580 var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); 1581 if ( widgetControl ) { 1582 widgetControls.push( widgetControl ); 1583 } 1584 }); 1555 1585 1556 this.$sectionContent.find( '.customize-control-widget_form:first' ) 1586 if ( ! widgetControls.length ) { 1587 return; 1588 } 1589 1590 $( widgetControls ).each( function () { 1591 $( this.container ) 1592 .removeClass( 'first-widget' ) 1593 .removeClass( 'last-widget' ) 1594 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); 1595 }); 1596 1597 _.first( widgetControls ).container 1557 1598 .addClass( 'first-widget' ) 1558 1599 .find( '.move-widget-up' ).prop( 'tabIndex', -1 ); 1559 1600 1560 this.$sectionContent.find( '.customize-control-widget_form:last' )1601 _.last( widgetControls ).container 1561 1602 .addClass( 'last-widget' ) 1562 1603 .find( '.move-widget-down' ).prop( 'tabIndex', -1 ); 1563 1604 }, … … 1571 1612 * Enable/disable the reordering UI 1572 1613 * 1573 1614 * @param {Boolean} showOrHide to enable/disable reordering 1615 * 1616 * @todo We should have a reordering state instead and rename this to onChangeReordering 1574 1617 */ 1575 1618 toggleReordering: function( showOrHide ) { 1576 1619 showOrHide = Boolean( showOrHide ); … … 1584 1627 1585 1628 if ( showOrHide ) { 1586 1629 _( this.getWidgetFormControls() ).each( function( formControl ) { 1587 formControl.collapse Form();1630 formControl.collapse(); 1588 1631 } ); 1589 1632 1590 1633 this.$sectionContent.find( '.first-widget .move-widget' ).focus(); … … 1651 1694 1652 1695 $widget = $( controlHtml ); 1653 1696 1697 // @todo need to pass this in as the control's 'content' property 1654 1698 $control = $( '<li/>' ) 1655 1699 .addClass( 'customize-control' ) 1656 1700 .addClass( 'customize-control-' + controlType ) … … 1674 1718 } 1675 1719 $control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); 1676 1720 1721 // @todo Eliminate this 1677 1722 this.container.after( $control ); 1678 1723 1679 1724 // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget) … … 1733 1778 1734 1779 $control.slideDown( function() { 1735 1780 if ( isExistingWidget ) { 1736 widgetFormControl.expand Form();1781 widgetFormControl.expand(); 1737 1782 widgetFormControl.updateWidget( { 1738 1783 instance: widgetFormControl.setting(), 1739 1784 complete: function( error ) { -
src/wp-includes/class-wp-customize-control.php
diff --git src/wp-includes/class-wp-customize-control.php src/wp-includes/class-wp-customize-control.php index cbb7cdd..8f24601 100644
class WP_Customize_Control { 74 74 public $input_attrs = array(); 75 75 76 76 /** 77 * @deprecated It is better to just call the json() method 77 78 * @access public 78 79 * @var array 79 80 */ … … class WP_Customize_Control { 218 219 } 219 220 220 221 $this->json['type'] = $this->type; 222 $this->json['priority'] = $this->priority; 223 $this->json['active'] = $this->active(); 224 $this->json['section'] = $this->section; 225 $this->json['content'] = $this->get_content(); 221 226 $this->json['label'] = $this->label; 222 227 $this->json['description'] = $this->description; 223 $this->json['active'] = $this->active(); 228 } 229 230 /** 231 * Get the data to export to the client via JSON. 232 * 233 * @since 4.1.0 234 * 235 * @return array 236 */ 237 public function json() { 238 $this->to_json(); 239 return $this->json; 224 240 } 225 241 226 242 /** … … class WP_Customize_Control { 244 260 } 245 261 246 262 /** 263 * Get the control's content for insertion into the Customizer pane. 264 * 265 * @since 4.1.0 266 * 267 * @return string 268 */ 269 public final function get_content() { 270 ob_start(); 271 $this->maybe_render(); 272 $template = trim( ob_get_contents() ); 273 ob_end_clean(); 274 return $template; 275 } 276 277 /** 247 278 * Check capabilities and render the control. 248 279 * 249 280 * @since 3.4.0 … … class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control { 1071 1102 /** 1072 1103 * Widget Area Customize Control Class 1073 1104 * 1105 * @since 3.9.0 1074 1106 */ 1075 1107 class WP_Widget_Area_Customize_Control extends WP_Customize_Control { 1076 1108 public $type = 'sidebar_widgets'; … … class WP_Widget_Area_Customize_Control extends WP_Customize_Control { 1112 1144 1113 1145 /** 1114 1146 * Widget Form Customize Control Class 1147 * 1148 * @since 3.9.0 1115 1149 */ 1116 1150 class WP_Widget_Form_Customize_Control extends WP_Customize_Control { 1117 1151 public $type = 'widget_form'; -
src/wp-includes/class-wp-customize-manager.php
diff --git src/wp-includes/class-wp-customize-manager.php src/wp-includes/class-wp-customize-manager.php index 17f0bad..c7c3b4d 100644
final class WP_Customize_Manager { 498 498 $settings = array( 499 499 'values' => array(), 500 500 'channel' => wp_unslash( $_POST['customize_messenger_channel'] ), 501 'activePanels' => array(), 502 'activeSections' => array(), 501 503 'activeControls' => array(), 502 504 ); 503 505 … … final class WP_Customize_Manager { 511 513 foreach ( $this->settings as $id => $setting ) { 512 514 $settings['values'][ $id ] = $setting->js_value(); 513 515 } 516 foreach ( $this->panels as $id => $panel ) { 517 $settings['activePanels'][ $id ] = $panel->active(); 518 } 519 foreach ( $this->sections as $id => $section ) { 520 $settings['activeSections'][ $id ] = $section->active(); 521 } 514 522 foreach ( $this->controls as $id => $control ) { 515 523 $settings['activeControls'][ $id ] = $control->active(); 516 524 } … … final class WP_Customize_Manager { 911 919 912 920 if ( ! $section->panel ) { 913 921 // Top-level section. 914 $sections[ ] = $section;922 $sections[ $section->id ] = $section; 915 923 } else { 916 924 // This section belongs to a panel. 917 925 if ( isset( $this->panels [ $section->panel ] ) ) { 918 $this->panels[ $section->panel ]->sections[ ] = $section;926 $this->panels[ $section->panel ]->sections[ $section->id ] = $section; 919 927 } 920 928 } 921 929 } … … final class WP_Customize_Manager { 932 940 continue; 933 941 } 934 942 935 u sort( $panel->sections, array( $this, '_cmp_priority' ) );936 $panels[ ] = $panel;943 uasort( $panel->sections, array( $this, '_cmp_priority' ) ); 944 $panels[ $panel->id ] = $panel; 937 945 } 938 946 $this->panels = $panels; 939 947 -
src/wp-includes/class-wp-customize-panel.php
diff --git src/wp-includes/class-wp-customize-panel.php src/wp-includes/class-wp-customize-panel.php index f289cb7..201c4b9 100644
class WP_Customize_Panel { 83 83 public $sections; 84 84 85 85 /** 86 * @since 4.1.0 87 * @access public 88 * @var string 89 */ 90 public $type; 91 92 /** 93 * Callback. 94 * 95 * @since 4.1.0 96 * @access public 97 * 98 * @see WP_Customize_Section::active() 99 * 100 * @var callable Callback is called with one argument, the instance of 101 * WP_Customize_Section, and returns bool to indicate whether 102 * the section is active (such as it relates to the URL 103 * currently being previewed). 104 */ 105 public $active_callback = ''; 106 107 /** 86 108 * Constructor. 87 109 * 88 110 * Any supplied $args override class property defaults. … … class WP_Customize_Panel { 103 125 104 126 $this->manager = $manager; 105 127 $this->id = $id; 128 if ( empty( $this->active_callback ) ) { 129 $this->active_callback = array( $this, 'active_callback' ); 130 } 106 131 107 132 $this->sections = array(); // Users cannot customize the $sections array. 108 133 … … class WP_Customize_Panel { 110 135 } 111 136 112 137 /** 138 * Check whether panel is active to current Customizer preview. 139 * 140 * @since 4.1.0 141 * @access public 142 * 143 * @return bool Whether the panel is active to the current preview. 144 */ 145 public final function active() { 146 $panel = $this; 147 $active = call_user_func( $this->active_callback, $this ); 148 149 /** 150 * Filter response of WP_Customize_Panel::active(). 151 * 152 * @since 4.1.0 153 * 154 * @param bool $active Whether the Customizer panel is active. 155 * @param WP_Customize_Panel $panel WP_Customize_Panel instance. 156 */ 157 $active = apply_filters( 'customize_panel_active', $active, $panel ); 158 159 return $active; 160 } 161 162 /** 163 * Default callback used when invoking WP_Customize_Panel::active(). 164 * 165 * Subclasses can override this with their specific logic, or they may 166 * provide an 'active_callback' argument to the constructor. 167 * 168 * @since 4.1.0 169 * @access public 170 * 171 * @return bool Always true. 172 */ 173 public function active_callback() { 174 return true; 175 } 176 177 /** 178 * Gather the parameters passed to client JavaScript via JSON. 179 * 180 * @since 4.1.0 181 * 182 * @return array The array to be exported to the client as JSON 183 */ 184 public function json() { 185 $array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'type' ) ); 186 $array['content'] = $this->get_content(); 187 $array['active'] = $this->active(); 188 return $array; 189 } 190 191 /** 113 192 * Checks required user capabilities and whether the theme has the 114 193 * feature support required by the panel. 115 194 * … … class WP_Customize_Panel { 130 209 } 131 210 132 211 /** 212 * Get the panel's content template for insertion into the Customizer pane. 213 * 214 * @since 4.1.0 215 * 216 * @return string 217 */ 218 public final function get_content() { 219 ob_start(); 220 $this->maybe_render(); 221 $template = trim( ob_get_contents() ); 222 ob_end_clean(); 223 return $template; 224 } 225 226 /** 133 227 * Check capabilities and render the panel. 134 228 * 135 229 * @since 4.0.0 … … class WP_Customize_Panel { 189 283 */ 190 284 protected function render_content() { 191 285 ?> 192 <li class=" accordion-section control-section<?php if ( empty( $this->description ) ) echo ' cannot-expand';?>">286 <li class="panel-meta accordion-section control-section<?php if ( empty( $this->description ) ) { echo ' cannot-expand'; } ?>"> 193 287 <div class="accordion-section-title" tabindex="0"> 194 288 <span class="preview-notice"><?php 195 289 /* translators: %s is the site/panel title in the Customizer */ … … class WP_Customize_Panel { 203 297 <?php endif; ?> 204 298 </li> 205 299 <?php 206 foreach ( $this->sections as $section ) {207 $section->maybe_render();208 }209 300 } 210 301 } -
src/wp-includes/class-wp-customize-section.php
diff --git src/wp-includes/class-wp-customize-section.php src/wp-includes/class-wp-customize-section.php index d740ddb..3553285 100644
class WP_Customize_Section { 92 92 public $controls; 93 93 94 94 /** 95 * @since 4.1.0 96 * @access public 97 * @var string 98 */ 99 public $type; 100 101 /** 102 * Callback. 103 * 104 * @since 4.1.0 105 * @access public 106 * 107 * @see WP_Customize_Section::active() 108 * 109 * @var callable Callback is called with one argument, the instance of 110 * WP_Customize_Section, and returns bool to indicate whether 111 * the section is active (such as it relates to the URL 112 * currently being previewed). 113 */ 114 public $active_callback = ''; 115 116 /** 95 117 * Constructor. 96 118 * 97 119 * Any supplied $args override class property defaults. … … class WP_Customize_Section { 105 127 public function __construct( $manager, $id, $args = array() ) { 106 128 $keys = array_keys( get_object_vars( $this ) ); 107 129 foreach ( $keys as $key ) { 108 if ( isset( $args[ $key ] ) ) 130 if ( isset( $args[ $key ] ) ) { 109 131 $this->$key = $args[ $key ]; 132 } 110 133 } 111 134 112 135 $this->manager = $manager; 113 136 $this->id = $id; 137 if ( empty( $this->active_callback ) ) { 138 $this->active_callback = array( $this, 'active_callback' ); 139 } 114 140 115 141 $this->controls = array(); // Users cannot customize the $controls array. 116 142 … … class WP_Customize_Section { 118 144 } 119 145 120 146 /** 147 * Check whether section is active to current Customizer preview. 148 * 149 * @since 4.1.0 150 * @access public 151 * 152 * @return bool Whether the section is active to the current preview. 153 */ 154 public final function active() { 155 $section = $this; 156 $active = call_user_func( $this->active_callback, $this ); 157 158 /** 159 * Filter response of WP_Customize_Section::active(). 160 * 161 * @since 4.1.0 162 * 163 * @param bool $active Whether the Customizer section is active. 164 * @param WP_Customize_Section $section WP_Customize_Section instance. 165 */ 166 $active = apply_filters( 'customize_section_active', $active, $section ); 167 168 return $active; 169 } 170 171 /** 172 * Default callback used when invoking WP_Customize_Section::active(). 173 * 174 * Subclasses can override this with their specific logic, or they may 175 * provide an 'active_callback' argument to the constructor. 176 * 177 * @since 4.1.0 178 * @access public 179 * 180 * @return bool Always true. 181 */ 182 public function active_callback() { 183 return true; 184 } 185 186 /** 187 * Gather the parameters passed to client JavaScript via JSON. 188 * 189 * @since 4.1.0 190 * 191 * @return array The array to be exported to the client as JSON 192 */ 193 public function json() { 194 $array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'panel', 'type' ) ); 195 $array['content'] = $this->get_content(); 196 $array['active'] = $this->active(); 197 return $array; 198 } 199 200 /** 121 201 * Checks required user capabilities and whether the theme has the 122 202 * feature support required by the section. 123 203 * … … class WP_Customize_Section { 126 206 * @return bool False if theme doesn't support the section or user doesn't have the capability. 127 207 */ 128 208 public final function check_capabilities() { 129 if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) ) 209 if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) ) { 130 210 return false; 211 } 131 212 132 if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) ) 213 if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) ) { 133 214 return false; 215 } 134 216 135 217 return true; 136 218 } 137 219 138 220 /** 221 * Get the section's content template for insertion into the Customizer pane. 222 * 223 * @since 4.1.0 224 * 225 * @return string 226 */ 227 public final function get_content() { 228 ob_start(); 229 $this->maybe_render(); 230 $template = trim( ob_get_contents() ); 231 ob_end_clean(); 232 return $template; 233 } 234 235 /** 139 236 * Check capabilities and render the section. 140 237 * 141 238 * @since 3.4.0 142 239 */ 143 240 public final function maybe_render() { 144 if ( ! $this->check_capabilities() ) 241 if ( ! $this->check_capabilities() ) { 145 242 return; 243 } 146 244 147 245 /** 148 246 * Fires before rendering a Customizer section. … … class WP_Customize_Section { 172 270 */ 173 271 protected function render() { 174 272 $classes = 'control-section accordion-section'; 175 if ( $this->panel ) {176 $classes .= ' control-subsection';177 }178 273 ?> 179 274 <li id="accordion-section-<?php echo esc_attr( $this->id ); ?>" class="<?php echo esc_attr( $classes ); ?>"> 180 275 <h3 class="accordion-section-title" tabindex="0"> … … class WP_Customize_Section { 183 278 </h3> 184 279 <ul class="accordion-section-content"> 185 280 <?php if ( ! empty( $this->description ) ) : ?> 186 <li><p class="description customize-section-description"><?php echo $this->description; ?></p></li> 281 <li class="customize-section-description-container"> 282 <p class="description customize-section-description"><?php echo $this->description; ?></p> 283 </li> 187 284 <?php endif; ?> 188 <?php189 foreach ( $this->controls as $control )190 $control->maybe_render();191 ?>192 285 </ul> 193 286 </li> 194 287 <?php -
src/wp-includes/js/customize-base.js
diff --git src/wp-includes/js/customize-base.js src/wp-includes/js/customize-base.js index d2488dd..6ea2822 100644
window.wp = window.wp || {}; 184 184 to = this.validate( to ); 185 185 186 186 // Bail if the sanitized value is null or unchanged. 187 if ( null === to || _.isEqual( from, to ) ) 187 if ( null === to || _.isEqual( from, to ) ) { 188 188 return this; 189 } 189 190 190 191 this._value = to; 191 192 this._dirty = true; -
src/wp-includes/js/customize-preview.js
diff --git src/wp-includes/js/customize-preview.js src/wp-includes/js/customize-preview.js index 6da26f4..1a82565 100644
107 107 }); 108 108 109 109 preview.send( 'ready', { 110 activePanels: api.settings.activePanels, 111 activeSections: api.settings.activeSections, 110 112 activeControls: api.settings.activeControls 111 113 } ); 112 114 -
tests/qunit/index.html
diff --git tests/qunit/index.html tests/qunit/index.html index c5afd52..ce11144 100644
8 8 <script src="../../src/wp-includes/js/underscore.min.js"></script> 9 9 <script src="../../src/wp-includes/js/backbone.min.js"></script> 10 10 <script src="../../src/wp-includes/js/zxcvbn.min.js"></script> 11 11 12 12 <!-- QUnit --> 13 13 <link rel="stylesheet" href="vendor/qunit.css" type="text/css" media="screen" /> 14 14 <script src="vendor/qunit.js"></script> … … 28 28 29 29 <!-- Tested files --> 30 30 <script src="../../src/wp-admin/js/password-strength-meter.js"></script> 31 <script src="../../src/wp-includes/js/customize-base.js"></script> 31 32 <script src="../../src/wp-includes/js/customize-models.js"></script> 32 33 <script src="../../src/wp-includes/js/shortcode.js"></script> 33 34 34 35 <!-- Unit tests --> 35 36 <script src="wp-admin/js/password-strength-meter.js"></script> 37 <script src="wp-admin/js/customize-base.js"></script> 36 38 <script src="wp-admin/js/customize-header.js"></script> 37 39 <script src="wp-includes/js/shortcode.js"></script> 38 40 </div> 39 41 </body> 40 42 </html> 41 -
new file tests/qunit/wp-admin/js/customize-base.js
diff --git tests/qunit/wp-admin/js/customize-base.js tests/qunit/wp-admin/js/customize-base.js new file mode 100644 index 0000000..c253940
- + 1 /* global wp, sinon */ 2 3 jQuery( function( $ ) { 4 var FooSuperClass, BarSubClass, foo, bar; 5 6 module( 'Customize Base: Class' ); 7 8 FooSuperClass = wp.customize.Class.extend( 9 { 10 initialize: function ( instanceProps ) { 11 $.extend( this, instanceProps || {} ); 12 }, 13 protoProp: 'protoPropValue' }, 14 { 15 staticProp: 'staticPropValue' 16 } 17 ); 18 test( 'FooSuperClass is a function ', function () { 19 equal( typeof FooSuperClass, 'function' ); 20 }); 21 test( 'FooSuperClass prototype has protoProp', function () { 22 equal( FooSuperClass.prototype.protoProp, 'protoPropValue' ); 23 }); 24 test( 'FooSuperClass does not have protoProp', function () { 25 equal( typeof FooSuperClass.protoProp, 'undefined' ); 26 }); 27 test( 'FooSuperClass has staticProp', function () { 28 equal( FooSuperClass.staticProp, 'staticPropValue' ); 29 }); 30 test( 'FooSuperClass prototype does not have staticProp', function () { 31 equal( typeof FooSuperClass.prototype.staticProp, 'undefined' ); 32 }); 33 34 foo = new FooSuperClass( { instanceProp: 'instancePropValue' } ); 35 test( 'FooSuperClass instance foo extended Class', function () { 36 equal( foo.extended( wp.customize.Class ), true ); 37 }); 38 test( 'foo instance has protoProp', function () { 39 equal( foo.protoProp, 'protoPropValue' ); 40 }); 41 test( 'foo instance does not have staticProp', function () { 42 equal( typeof foo.staticProp, 'undefined' ); 43 }); 44 test( 'FooSuperClass instance foo ran initialize() and has supplied instanceProp', function () { 45 equal( foo.instanceProp, 'instancePropValue' ); 46 }); 47 48 // @todo Test Class.constructor() manipulation 49 // @todo Test Class.applicator? 50 // @todo do we test object.instance? 51 52 53 module( 'Customize Base: Subclass' ); 54 55 BarSubClass = FooSuperClass.extend( 56 { 57 initialize: function ( instanceProps ) { 58 FooSuperClass.prototype.initialize.call( this, instanceProps ); 59 this.subInstanceProp = 'subInstancePropValue'; 60 }, 61 subProtoProp: 'subProtoPropValue' 62 }, 63 { 64 subStaticProp: 'subStaticPropValue' 65 } 66 ); 67 test( 'BarSubClass prototype has subProtoProp', function () { 68 equal( BarSubClass.prototype.subProtoProp, 'subProtoPropValue' ); 69 }); 70 test( 'BarSubClass prototype has parent FooSuperClass protoProp', function () { 71 equal( BarSubClass.prototype.protoProp, 'protoPropValue' ); 72 }); 73 74 bar = new BarSubClass( { instanceProp: 'instancePropValue' } ); 75 test( 'BarSubClass instance bar its initialize() and parent initialize() run', function () { 76 equal( bar.instanceProp, 'instancePropValue' ); 77 equal( bar.subInstanceProp, 'subInstancePropValue' ); 78 }); 79 80 test( 'BarSubClass instance bar extended FooSuperClass', function () { 81 equal( bar.extended( FooSuperClass ), true ); 82 }); 83 84 });