Ticket #28709: 28709.wip.7.diff
File 28709.wip.7.diff, 58.7 KB (added by , 10 years ago) |
---|
-
src/wp-admin/customize.php
diff --git src/wp-admin/customize.php src/wp-admin/customize.php index 7828ee4..85caff4 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' ); 249 243 ), 250 244 'settings' => array(), 251 245 'controls' => array(), 246 'panels' => array(), 247 'sections' => array(), 252 248 'nonce' => array( 253 249 'save' => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ), 254 250 'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() ) … … do_action( 'customize_controls_print_scripts' ); 263 259 ); 264 260 } 265 261 266 // Prepare Customize Control objects to pass to Java script.262 // Prepare Customize Control objects to pass to JavaScript. 267 263 foreach ( $wp_customize->controls() as $id => $control ) { 268 $control->to_json(); 269 $settings['controls'][ $id ] = $control->json; 264 $settings['controls'][ $id ] = $control->json(); 265 } 266 267 // Prepare Customize Section objects to pass to JavaScript. 268 foreach ( $wp_customize->sections() as $id => $section ) { 269 $settings['sections'][ $id ] = $section->json(); 270 } 271 272 // Prepare Customize Panel objects to pass to JavaScript. 273 foreach ( $wp_customize->panels() as $id => $panel ) { 274 $settings['panels'][ $id ] = $panel->json(); 275 foreach ( $panel->sections as $section_id => $section ) { 276 $settings['sections'][ $section_id ] = $section->json(); 277 } 270 278 } 271 279 272 280 ?> -
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 fad223e..0a12f5b 100644
1 1 /* globals _wpCustomizeHeader, _wpMediaViewsL10n */ 2 2 (function( exports, $ ){ 3 var api = wp.customize;3 var bubbleChildValueChanges, Container, 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 * Base class for Panel and Section 51 * 34 52 * @constructor 35 53 * @augments wp.customize.Class 36 54 */ 37 api.Control = api.Class.extend({ 38 initialize: function( id, options ) { 39 var control = this, 40 nodes, radios, settings; 55 Container = api.Class.extend({ 56 defaultActiveArguments: { duration: null }, 57 defaultExpandedArguments: { duration: 150 }, 58 59 initialize: function ( id, options ) { 60 var container = this; 61 container.id = id; 62 container.params = {}; 63 $.extend( container, options || {} ); 64 container.container = $( container.params.content ); 65 66 container.priority = new api.Value(); 67 container.active = new api.Value(); 68 container.activeArgumentsQueue = []; 69 container.expanded = new api.Value(); 70 container.expandedArgumentsQueue = []; 71 72 container.active.bind( function ( active ) { 73 var args = container.activeArgumentsQueue.shift(); 74 args = $.extend( {}, container.defaultActiveArguments, args ); 75 active = ( active && container.isContextuallyActive() ); 76 container.onToggleActive( active, args ); 77 // @todo trigger 'activated' and 'deactivated' events based on the expanded param? 78 }); 79 container.expanded.bind( function ( expanded ) { 80 var args = container.expandedArgumentsQueue.shift(); 81 args = $.extend( {}, container.defaultExpandedArguments, args ); 82 container.onToggleExpanded( expanded, args ); 83 // @todo trigger 'expanded' and 'collapsed' events based on the expanded param? 84 }); 41 85 42 this.params = {}; 43 $.extend( this, options || {} ); 86 container.attachEvents(); 44 87 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 ); 88 bubbleChildValueChanges( container, [ 'priority', 'active' ] ); 49 89 50 settings = $.map( this.params.settings, function( value ) { 51 return value; 90 container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority ); 91 container.active.set( container.params.active ); 92 container.expanded.set( false ); // @todo True if deeplinking? 93 }, 94 95 /** 96 * Get the child models associated with this parent, sorting them by their priority Value. 97 * 98 * @param {String} parentType 99 * @param {String} childType 100 * @returns {Array} 101 */ 102 _children: function ( parentType, childType ) { 103 var parent = this, 104 children = []; 105 api[ childType ].each( function ( child ) { 106 if ( child[ parentType ].get() === parent.id ) { 107 children.push( child ); 108 } 109 } ); 110 children.sort( function ( a, b ) { 111 return a.priority() - b.priority(); 112 } ); 113 return children; 114 }, 115 116 /** 117 * To override by subclass, to return whether the container has active children. 118 */ 119 isContextuallyActive: function () { 120 throw new Error( 'Must override with subclass.' ); 121 }, 122 123 /** 124 * Handle changes to the active state. 125 * This does not change the active state, it merely handles the behavior 126 * for when it does change. 127 * 128 * To override by subclass, update the container's UI to reflect the provided active state. 129 * 130 * @param {Boolean} active 131 * @param {Object} args merged on top of this.defaultActiveArguments 132 */ 133 onToggleActive: function ( active, args ) { 134 if ( active ) { 135 this.container.stop( true, true ).slideDown( args.duration ); // @todo pass args.completeCallback 136 } else { 137 this.container.stop( true, true ).slideUp( args.duration ); // @todo pass args.completeCallback 138 } 139 }, 140 141 /** 142 * @param {Object} [params] 143 */ 144 activate: function ( params ) { 145 this.activeArgumentsQueue.push( params || {} ); 146 this.active.set( true ); 147 }, 148 149 /** 150 * @param {Object} [params] 151 */ 152 deactivate: function ( params ) { 153 this.activeArgumentsQueue.push( params || {} ); 154 this.active.set( false ); 155 }, 156 157 /** 158 * To override by subclass, update the container's UI to reflect the provided active state. 159 */ 160 onToggleExpanded: function () { 161 throw new Error( 'Must override with subclass.' ); 162 }, 163 164 /** 165 * @param {Object} [params] 166 */ 167 expand: function ( params ) { 168 this.expandedArgumentsQueue.push( params || {} ); 169 this.expanded.set( true ); 170 }, 171 172 /** 173 * @param {Object} [params] 174 */ 175 collapse: function ( params ) { 176 this.expandedArgumentsQueue.push( params || {} ); 177 this.expanded( false ); 178 }, 179 180 /** 181 * 182 */ 183 focus: function () { 184 throw new Error( 'focus method must be overridden' ); 185 } 186 }); 187 188 /** 189 * @constructor 190 * @augments wp.customize.Class 191 */ 192 api.Section = Container.extend({ 193 194 /** 195 * @param {String} id 196 * @param {Array} options 197 */ 198 initialize: function ( id, options ) { 199 var section = this; 200 Container.prototype.initialize.call( section, id, options ); 201 202 section.panel = new api.Value(); 203 section.panel.bind( function ( id ) { 204 $( section.container ).toggleClass( 'control-subsection', !! id ); 52 205 }); 206 section.panel.set( section.params.panel || '' ); 207 bubbleChildValueChanges( section, [ 'panel' ] ); 208 }, 53 209 54 api.apply( api, settings.concat( function() { 55 var key; 210 /** 211 * 212 */ 213 embed: function ( readyCallback ) { 214 var panel_id, 215 section = this; 216 217 panel_id = this.panel.get(); 218 if ( ! panel_id ) { 219 $( '#customize-theme-controls > ul' ).append( section.container ); 220 readyCallback(); 221 } else { 222 api.panel( panel_id, function ( panel ) { 223 panel.embed(); 224 panel.container.find( 'ul:first' ).append( section.container ); 225 readyCallback(); 226 } ); 227 } 228 }, 56 229 57 control.settings = {}; 58 for ( key in control.params.settings ) { 59 control.settings[ key ] = api( control.params.settings[ key ] ); 230 /** 231 * Add behaviors for the accordion section 232 */ 233 attachEvents: function () { 234 var section = this; 235 236 // Expand/Collapse accordion sections on click. 237 section.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) { 238 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 239 return; 60 240 } 241 e.preventDefault(); // Keep this AFTER the key filter above 61 242 62 control.setting = control.settings['default'] || null; 63 control.ready(); 64 }) ); 243 if ( section.expanded() ) { 244 section.collapse(); 245 } else { 246 section.expand(); 247 } 248 }); 249 }, 250 251 /** 252 * Return whether this section has any active controls. 253 * 254 * @returns {boolean} 255 */ 256 isContextuallyActive: function () { 257 var section = this, 258 controls = section.controls(), 259 activeCount = 0; 260 _( controls ).each( function ( control ) { 261 if ( control.active() ) { 262 activeCount += 1; 263 } 264 } ); 265 return ( activeCount !== 0 ); 266 }, 267 268 /** 269 * Get the controls that are associated with this section, sorted by their priority Value. 270 * 271 * @returns {Array} 272 */ 273 controls: function () { 274 return this._children( 'section', 'control' ); 275 }, 276 277 /** 278 * Update UI to reflect expanded state 279 * 280 * @param {Boolean} expanded 281 * @param {Object} args 282 */ 283 onToggleExpanded: function ( expanded, args ) { 284 var section = this, 285 content = section.container.find( '.accordion-section-content' ); 286 287 if ( expanded ) { 288 289 if ( section.panel() ) { 290 api.panel( section.panel() ).expand(); 291 } 292 293 api.section.each( function ( otherSection ) { 294 if ( otherSection !== section ) { 295 otherSection.collapse( { duration : 0 } ); 296 } 297 }); 298 299 content.stop().slideDown( args.duration ); // @todo pass args.completeCallback 300 section.container.addClass( 'open' ); 301 } else { 302 303 section.container.removeClass( 'open' ); 304 content.slideUp( args.duration ); // @todo pass args.completeCallback 305 } 306 }, 307 308 /** 309 * Bring the containing panel into view and then expand this section and bring it into view 310 * 311 * @todo This is an alias for expand(); do we need it? 312 */ 313 focus: function () { 314 var section = this; 315 // @todo What if it is not active? Return false? 316 section.expand(); 317 } 318 }); 319 320 /** 321 * @constructor 322 * @augments wp.customize.Class 323 */ 324 api.Panel = Container.extend({ 325 initialize: function ( id, options ) { 326 var panel = this; 327 Container.prototype.initialize.call( panel, id, options ); 328 }, 329 330 /** 331 * 332 */ 333 embed: function ( readyCallback ) { 334 $( '#customize-theme-controls > ul' ).append( this.container ); 335 if ( readyCallback ) { 336 readyCallback(); 337 } 338 }, 339 340 /** 341 * 342 */ 343 attachEvents: function () { 344 var meta, panel = this; 345 346 // Expand/Collapse accordion sections on click. 347 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) { 348 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 349 return; 350 } 351 e.preventDefault(); // Keep this AFTER the key filter above 352 353 if ( ! panel.expanded() ) { 354 panel.expand(); 355 } 356 }); 357 358 meta = panel.container.find( '.panel-meta:first' ); 359 360 meta.find( '> .accordion-section-title' ).on( 'click keydown', function( e ) { 361 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 362 return; 363 } 364 e.preventDefault(); // Keep this AFTER the key filter above 365 366 if ( meta.hasClass( 'cannot-expand' ) ) { 367 return; 368 } 369 370 var content = meta.find( '.accordion-section-content:first' ); 371 if ( meta.hasClass( 'open' ) ) { 372 meta.toggleClass( 'open' ); 373 content.slideUp( 150 ); 374 } else { 375 content.slideDown( 150 ); 376 meta.toggleClass( 'open' ); 377 } 378 }); 379 380 }, 381 382 /** 383 * Get the sections that are associated with this panel, sorted by their priority Value. 384 * 385 * @returns {Array} 386 */ 387 sections: function () { 388 return this._children( 'panel', 'section' ); 389 }, 390 391 /** 392 * Return whether this section has any active sections. 393 * 394 * @returns {boolean} 395 */ 396 isContextuallyActive: function () { 397 var panel = this, 398 sections = panel.sections(), 399 activeCount = 0; 400 _( sections ).each( function ( section ) { 401 if ( section.active() && section.isContextuallyActive() ) { 402 activeCount += 1; 403 } 404 } ); 405 return ( activeCount !== 0 ); 406 }, 407 408 /** 409 * Update UI to reflect expanded state 410 * 411 * @param {Boolean} expanded 412 */ 413 onToggleExpanded: function ( expanded ) { 414 // Note: there is a second argument 'args' passed 415 var position, scroll, 416 panel = this, 417 section = panel.container.closest( '.accordion-section' ), 418 overlay = section.closest( '.wp-full-overlay' ), 419 container = section.closest( '.accordion-container' ), 420 siblings = container.find( '.open' ), 421 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), 422 backBtn = overlay.find( '.control-panel-back' ), 423 panelTitle = section.find( '.accordion-section-title' ).first(), 424 content = section.find( '.control-panel-content' ); 425 426 if ( expanded ) { 427 428 // Collapse any sibling sections/panels 429 api.section.each( function ( section ) { 430 if ( ! section.panel() ) { 431 section.collapse( { duration: 0 } ); 432 } 433 }); 434 api.panel.each( function ( otherPanel ) { 435 if ( panel !== otherPanel ) { 436 otherPanel.collapse( { duration: 0 } ); 437 } 438 }); 439 440 content.show( 0, function() { 441 position = content.offset().top; 442 scroll = container.scrollTop(); 443 content.css( 'margin-top', ( 45 - position - scroll ) ); 444 section.addClass( 'current-panel' ); 445 overlay.addClass( 'in-sub-panel' ); 446 container.scrollTop( 0 ); 447 } ); 448 topPanel.attr( 'tabindex', '-1' ); 449 backBtn.attr( 'tabindex', '0' ); 450 backBtn.focus(); 451 } else { 452 siblings.removeClass( 'open' ); 453 section.removeClass( 'current-panel' ); 454 overlay.removeClass( 'in-sub-panel' ); 455 content.delay( 180 ).hide( 0, function() { 456 content.css( 'margin-top', 'inherit' ); // Reset 457 } ); 458 topPanel.attr( 'tabindex', '0' ); 459 backBtn.attr( 'tabindex', '-1' ); 460 panelTitle.focus(); 461 container.scrollTop( 0 ); 462 } 463 }, 464 465 /** 466 * Bring the containing panel into view and then expand this section and bring it into view 467 */ 468 focus: function () { 469 var panel = this; 470 // @todo What if it is not active? Return false? 471 panel.expand(); 472 } 473 474 // @todo Need to first exit out of the Panel 475 }); 476 477 /** 478 * @constructor 479 * @augments wp.customize.Class 480 */ 481 api.Control = api.Class.extend({ 482 defaultActiveArguments: { duration: null }, 483 484 initialize: function( id, options ) { 485 var control = this, 486 nodes, radios, settings; 487 488 control.params = {}; 489 $.extend( control, options || {} ); 490 491 control.id = id; 492 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); 493 control.container = control.params.content ? $( control.params.content ) : $( control.selector ); 494 495 control.section = new api.Value(); 496 control.priority = new api.Value(); 497 control.active = new api.Value(); 498 control.activeArgumentsQueue = []; 65 499 66 500 control.elements = []; 67 501 68 nodes = this.container.find('[data-customize-setting-link]');502 nodes = control.container.find('[data-customize-setting-link]'); 69 503 radios = {}; 70 504 71 505 nodes.each( function() { 72 var node = $( this),506 var node = $( this ), 73 507 name; 74 508 75 if ( node.is( ':radio') ) {76 name = node.prop( 'name');77 if ( radios[ name ] ) 509 if ( node.is( ':radio' ) ) { 510 name = node.prop( 'name' ); 511 if ( radios[ name ] ) { 78 512 return; 513 } 79 514 80 515 radios[ name ] = true; 81 516 node = nodes.filter( '[name="' + name + '"]' ); 82 517 } 83 518 84 api( node.data( 'customizeSettingLink'), function( setting ) {519 api( node.data( 'customizeSettingLink' ), function( setting ) { 85 520 var element = new api.Element( node ); 86 521 control.elements.push( element ); 87 522 element.sync( setting ); … … 90 525 }); 91 526 92 527 control.active.bind( function ( active ) { 93 control.toggle( active ); 528 var args = control.activeArgumentsQueue.shift(); 529 args = $.extend( {}, control.defaultActiveArguments, args ); 530 control.onToggleActive( active, args ); 531 } ); 532 533 control.section.set( control.params.section ); 534 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority ); 535 control.active.set( control.params.active ); 536 537 bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] ); 538 539 // Associate this control with its settings when they are created 540 settings = $.map( control.params.settings, function( value ) { 541 return value; 542 }); 543 api.apply( api, settings.concat( function () { 544 var key; 545 546 control.settings = {}; 547 for ( key in control.params.settings ) { 548 control.settings[ key ] = api( control.params.settings[ key ] ); 549 } 550 551 control.setting = control.settings['default'] || null; 552 control.embed( function () { 553 control.ready(); 554 }); 555 }) ); 556 }, 557 558 /** 559 * @param {Function} [readyCallback] Callback to fire when the embedding is done. 560 */ 561 embed: function ( readyCallback ) { 562 var section_id, 563 control = this; 564 565 section_id = control.section.get(); 566 if ( ! section_id ) { 567 throw new Error( 'A control must have an associated section.' ); 568 } 569 570 // Defer until the associated section is available 571 api.section( section_id, function ( section ) { 572 section.embed( function () { 573 section.container.find( 'ul:first' ).append( control.container ); 574 readyCallback(); 575 } ); 94 576 } ); 95 control.toggle( control.active() );96 577 }, 97 578 98 579 /** … … 101 582 ready: function() {}, 102 583 103 584 /** 104 * Callback for change to the control's active state. 105 * 106 * Override function for custom behavior for the control being active/inactive. 585 * Bring the containing section and panel into view and then this control into view, focusing on the first input 586 */ 587 focus: function () { 588 throw new Error( 'Not implemented yet' ); 589 }, 590 591 /** 592 * Update UI in response to a change in the control's active state. 593 * This does not change the active state, it merely handles the behavior 594 * for when it does change. 107 595 * 108 596 * @param {Boolean} active 597 * @param {Object} args merged on top of this.defaultActiveArguments 109 598 */ 110 toggle: function ( active) {599 onToggleActive: function ( active, args ) { 111 600 if ( active ) { 112 this.container.slideDown( );601 this.container.slideDown( args.duration ); 113 602 } else { 114 this.container.slideUp( );603 this.container.slideUp( args.duration ); 115 604 } 116 605 }, 117 606 607 /** 608 * @deprecated alias of onToggleActive 609 */ 610 toggle: function ( active ) { 611 return this.onToggleActive( active, this.defaultActiveArguments ); 612 }, 613 614 /** 615 * Shorthand way to enable the active state. 616 */ 617 activate: function () { 618 this.active.set( true ); 619 }, 620 621 /** 622 * Shorthand way to disable the active state. 623 */ 624 deactivate: function () { 625 this.active.set( false ); 626 }, 627 118 628 dropdownInit: function() { 119 629 var control = this, 120 630 statuses = this.container.find('.dropdown-status'), … … 575 1085 576 1086 // Create the collection of Control objects. 577 1087 api.control = new api.Values({ defaultConstructor: api.Control }); 1088 api.section = new api.Values({ defaultConstructor: api.Section }); 1089 api.panel = new api.Values({ defaultConstructor: api.Panel }); 578 1090 579 1091 /** 580 1092 * @constructor … … 610 1122 loaded = false, 611 1123 ready = false; 612 1124 613 if ( this._ready ) 1125 if ( this._ready ) { 614 1126 this.unbind( 'ready', this._ready ); 1127 } 615 1128 616 1129 this._ready = function() { 617 1130 ready = true; 618 1131 619 if ( loaded ) 1132 if ( loaded ) { 620 1133 deferred.resolveWith( self ); 1134 } 621 1135 }; 622 1136 623 1137 this.bind( 'ready', this._ready ); 624 1138 625 1139 this.bind( 'ready', function ( data ) { 626 if ( ! data || ! data.activeControls) {1140 if ( ! data ) { 627 1141 return; 628 1142 } 629 1143 630 $.each( data.activeControls, function ( id, active ) { 631 var control = api.control( id ); 632 if ( control ) { 633 control.active( active ); 1144 var constructs = { 1145 panel: data.activePanels, 1146 section: data.activeSections, 1147 control: data.activeControls 1148 }; 1149 1150 $.each( constructs, function ( type, activeConstructs ) { 1151 if ( activeConstructs ) { 1152 $.each( activeConstructs, function ( id, active ) { 1153 var construct = api[ type ]( id ); 1154 if ( construct ) { 1155 construct.active( active ); 1156 } 1157 } ); 634 1158 } 635 1159 } ); 1160 636 1161 } ); 637 1162 638 1163 this.request = $.ajax( this.previewUrl(), { … … 654 1179 655 1180 // Check if the location response header differs from the current URL. 656 1181 // If so, the request was redirected; try loading the requested page. 657 if ( location && location != self.previewUrl() ) {1182 if ( location && location !== self.previewUrl() ) { 658 1183 deferred.rejectWith( self, [ 'redirect', location ] ); 659 1184 return; 660 1185 } … … 979 1504 image: api.ImageControl, 980 1505 header: api.HeaderControl 981 1506 }; 1507 api.panelConstructor = {}; 1508 api.sectionConstructor = {}; 982 1509 983 1510 $( function() { 984 1511 api.settings = window._wpCustomizeSettings; … … 1009 1536 } 1010 1537 }); 1011 1538 1539 // Expand/Collapse the main customizer customize info 1540 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( e ) { 1541 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 1542 return; 1543 } 1544 e.preventDefault(); // Keep this AFTER the key filter above 1545 1546 var section = $( this ).parent(), 1547 content = section.find( '.accordion-section-content:first' ); 1548 1549 if ( section.hasClass( 'cannot-expand' ) ) { 1550 return; 1551 } 1552 1553 if ( section.hasClass( 'open' ) ) { 1554 section.toggleClass( 'open' ); 1555 content.slideUp( 150 ); 1556 } else { 1557 content.slideDown( 150 ); 1558 section.toggleClass( 'open' ); 1559 } 1560 }); 1561 1012 1562 // Initialize Previewer 1013 1563 api.previewer = new api.Previewer({ 1014 1564 container: '#customize-preview', … … 1102 1652 $.extend( this.nonce, nonce ); 1103 1653 }); 1104 1654 1655 // Create Settings 1105 1656 $.each( api.settings.settings, function( id, data ) { 1106 1657 api.create( id, id, data.value, { 1107 1658 transport: data.transport, … … 1109 1660 } ); 1110 1661 }); 1111 1662 1663 // Create Panels 1664 $.each( api.settings.panels, function ( id, data ) { 1665 var constructor = api.panelConstructor[ data.type ] || api.Panel, 1666 panel; 1667 1668 panel = new constructor( id, { 1669 params: data 1670 } ); 1671 api.panel.add( id, panel ); 1672 }); 1673 1674 // Create Sections 1675 $.each( api.settings.sections, function ( id, data ) { 1676 var constructor = api.sectionConstructor[ data.type ] || api.Section, 1677 section; 1678 1679 section = new constructor( id, { 1680 params: data 1681 } ); 1682 api.section.add( id, section ); 1683 }); 1684 1685 // Create Controls 1686 // @todo factor this out 1112 1687 $.each( api.settings.controls, function( id, data ) { 1113 1688 var constructor = api.controlConstructor[ data.type ] || api.Control, 1114 1689 control; 1115 1690 1116 control = api.control.add( id,new constructor( id, {1691 control = new constructor( id, { 1117 1692 params: data, 1118 1693 previewer: api.previewer 1119 } ) ); 1694 } ); 1695 api.control.add( id, control ); 1120 1696 }); 1121 1697 1698 /** 1699 * Sort panels, sections, controls by priorities. Hide empty sections and panels. 1700 */ 1701 api.reflowPaneContents = _.bind( function () { 1702 1703 var appendContainer, activeElement, rootNodes = []; 1704 1705 if ( document.activeElement ) { 1706 activeElement = $( document.activeElement ); 1707 } 1708 1709 api.panel.each( function ( panel ) { 1710 var sections = panel.sections(); 1711 rootNodes.push( panel ); 1712 appendContainer = panel.container.find( 'ul:first' ); 1713 // @todo Skip doing any DOM manipulation if the ordering is already correct 1714 _( sections ).each( function ( section ) { 1715 appendContainer.append( section.container ); 1716 } ); 1717 } ); 1718 1719 api.section.each( function ( section ) { 1720 var controls = section.controls(); 1721 if ( ! section.panel() ) { 1722 rootNodes.push( section ); 1723 } 1724 appendContainer = section.container.find( 'ul:first' ); 1725 // @todo Skip doing any DOM manipulation if the ordering is already correct 1726 _( controls ).each( function ( control ) { 1727 appendContainer.append( control.container ); 1728 } ); 1729 } ); 1730 1731 // Sort the root elements 1732 rootNodes.sort( function ( a, b ) { 1733 return a.priority() - b.priority(); 1734 } ); 1735 appendContainer = $( '#customize-theme-controls > ul' ); 1736 // @todo Skip doing any DOM manipulation if the ordering is already correct 1737 _( rootNodes ).each( function ( rootNode ) { 1738 appendContainer.append( rootNode.container ); 1739 } ); 1740 1741 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered 1742 api.panel.each( function ( panel ) { 1743 var value = panel.active(); 1744 panel.active.callbacks.fireWith( panel.active, [ value, value ] ); 1745 } ); 1746 api.section.each( function ( section ) { 1747 var value = section.active(); 1748 section.active.callbacks.fireWith( section.active, [ value, value ] ); 1749 } ); 1750 1751 if ( activeElement ) { 1752 activeElement.focus(); 1753 } 1754 }, api ); 1755 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 ); 1756 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { 1757 values.bind( 'add', api.reflowPaneContents ); 1758 values.bind( 'change', api.reflowPaneContents ); 1759 values.bind( 'remove', api.reflowPaneContents ); 1760 } ); 1761 api.bind( 'ready', api.reflowPaneContents ); 1762 1122 1763 // Check if preview url is valid and load the preview frame. 1123 1764 if ( api.previewer.previewUrl() ) { 1124 1765 api.previewer.refresh(); … … 1183 1824 event.preventDefault(); 1184 1825 }); 1185 1826 1827 // Go back to the top-level Customizer accordion. 1828 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) { 1829 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 1830 return; 1831 } 1832 1833 e.preventDefault(); // Keep this AFTER the key filter above 1834 api.panel.each( function ( panel ) { 1835 panel.collapse(); 1836 }); 1837 }); 1838 1186 1839 closeBtn.keydown( function( event ) { 1187 1840 if ( 9 === event.which ) // tab 1188 1841 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..607926b 100644
404 404 * @augments wp.customize.Control 405 405 */ 406 406 api.Widgets.WidgetControl = api.Control.extend({ 407 defaultExpandedArguments: {}, 408 409 initialize: function ( id, options ) { 410 var control = this; 411 api.Control.prototype.initialize.call( control, id, options ); 412 control.expanded = new api.Value(); 413 control.expandedArgumentsQueue = []; 414 control.expanded.bind( function ( expanded ) { 415 var args = control.expandedArgumentsQueue.shift(); 416 args = $.extend( {}, control.defaultExpandedArguments, args ); 417 control.onToggleExpanded( expanded, args ); 418 }); 419 control.expanded.set( false ); 420 }, 421 407 422 /** 408 423 * Set up the control 409 424 */ … … 529 544 if ( sidebarWidgetsControl.isReordering ) { 530 545 return; 531 546 } 532 self. toggleForm();547 self.expanded( ! self.expanded() ); 533 548 } ); 534 549 535 550 $closeBtn = this.container.find( '.widget-control-close' ); 536 551 $closeBtn.on( 'click', function( e ) { 537 552 e.preventDefault(); 538 self.collapse Form();553 self.collapse(); 539 554 self.container.find( '.widget-top .widget-action:first' ).focus(); // keyboard accessibility 540 555 } ); 541 556 }, … … 778 793 * 779 794 * @param {Boolean} active 780 795 */ 781 toggle: function ( active ) { 796 onToggleActive: function ( active ) { 797 // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments 782 798 this.container.toggleClass( 'widget-rendered', active ); 783 799 }, 784 800 … … 1101 1117 * Expand the accordion section containing a control 1102 1118 */ 1103 1119 expandControlSection: function() { 1104 var $section = this.container.closest( '.accordion-section' ); 1120 api.section( this.section() ).expand(); 1121 }, 1105 1122 1106 if ( ! $section.hasClass( 'open' ) ) { 1107 $section.find( '.accordion-section-title:first' ).trigger( 'click' ); 1108 } 1123 /** 1124 * Expand the widget form control 1125 */ 1126 expand: function () { 1127 this.expanded( true ); 1109 1128 }, 1110 1129 1111 1130 /** 1112 1131 * Expand the widget form control 1132 * 1133 * @deprecated alias of expand() 1113 1134 */ 1114 1135 expandForm: function() { 1115 this.toggleForm( true ); 1136 this.expand(); 1137 }, 1138 1139 /** 1140 * Collapse the widget form control 1141 */ 1142 collapse: function () { 1143 this.expanded( false ); 1116 1144 }, 1117 1145 1118 1146 /** 1119 1147 * Collapse the widget form control 1148 * 1149 * @deprecated alias of expand() 1120 1150 */ 1121 1151 collapseForm: function() { 1122 this. toggleForm( false);1152 this.collapse(); 1123 1153 }, 1124 1154 1125 1155 /** 1126 1156 * Expand or collapse the widget control 1127 1157 * 1158 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) 1159 * 1128 1160 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility 1129 1161 */ 1130 1162 toggleForm: function( showOrHide ) { 1131 var self = this, $widget, $inside, complete; 1163 if ( typeof showOrHide === 'undefined' ) { 1164 showOrHide = ! this.expanded(); 1165 } 1166 this.expanded( showOrHide ); 1167 }, 1168 1169 /** 1170 * Respond to change in the expanded state. 1171 * 1172 * @param {Boolean} expanded 1173 * @param {Object} args merged on top of this.defaultActiveArguments 1174 */ 1175 onToggleExpanded: function ( expanded, args ) { 1132 1176 1177 var self = this, $widget, $inside, complete; 1133 1178 $widget = this.container.find( 'div.widget:first' ); 1134 1179 $inside = $widget.find( '.widget-inside:first' ); 1135 if ( typeof showOrHide === 'undefined' ) {1136 showOrHide = ! $inside.is( ':visible' );1137 }1138 1180 1139 // Already expanded or collapsed, so noop 1140 if ( $inside.is( ':visible' ) === showOrHide ) { 1141 return; 1142 } 1181 if ( expanded ) { 1182 1183 self.expandControlSection(); 1143 1184 1144 if ( showOrHide ) {1145 1185 // Close all other widget controls before expanding this one 1146 1186 api.control.each( function( otherControl ) { 1147 1187 if ( self.params.type === otherControl.params.type && self !== otherControl ) { 1148 otherControl.collapse Form();1188 otherControl.collapse(); 1149 1189 } 1150 1190 } ); 1151 1191 … … 1164 1204 self.container.trigger( 'expand' ); 1165 1205 self.container.addClass( 'expanding' ); 1166 1206 } else { 1207 1167 1208 complete = function() { 1168 1209 self.container.removeClass( 'collapsing' ); 1169 1210 self.container.removeClass( 'expanded' ); … … 1189 1230 * the first input in the control 1190 1231 */ 1191 1232 focus: function() { 1192 this.expandControlSection(); 1193 this.expandForm(); 1233 this.expand(); 1194 1234 this.container.find( '.widget-content :focusable:first' ).focus(); 1195 1235 }, 1196 1236 … … 1325 1365 registeredSidebar = api.Widgets.registeredSidebars.get( this.params.sidebar_id ); 1326 1366 1327 1367 this.setting.bind( function( newWidgetIds, oldWidgetIds ) { 1328 var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;1368 var widgetFormControls, removedWidgetIds, priority; 1329 1369 1330 1370 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); 1331 1371 … … 1350 1390 widgetFormControls.sort( function( a, b ) { 1351 1391 var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), 1352 1392 bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); 1393 return aIndex - bIndex; 1394 }); 1353 1395 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 ); 1396 priority = 0; 1397 _( widgetFormControls ).each( function ( control ) { 1398 control.priority( priority ); 1399 control.section( self.section() ); 1400 priority += 1; 1401 }); 1402 self.priority( priority ); // Make sure sidebar control remains at end 1368 1403 1369 1404 // Re-sort widget form controls (including widgets form other sidebars newly moved here) 1370 1405 self._applyCardinalOrderClassNames(); … … 1434 1469 // Update the model with whether or not the sidebar is rendered 1435 1470 self.active.bind( function ( active ) { 1436 1471 registeredSidebar.set( 'is_rendered', active ); 1472 api.section( self.section.get() ).active( active ); 1437 1473 } ); 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 } 1474 api.section( self.section.get() ).active( self.active() ); 1467 1475 }, 1468 1476 1469 1477 /** … … 1496 1504 /** 1497 1505 * Expand other Customizer sidebar section when dragging a control widget over it, 1498 1506 * allowing the control to be dropped into another section 1507 * 1508 * @todo The wp.customize.Section API should accomodate forcing a single accordion open 1499 1509 */ 1500 1510 this.$controlSection.find( '.accordion-section-title' ).droppable({ 1501 1511 accept: '.customize-control-widget_form', … … 1548 1558 * Add classes to the widget_form controls to assist with styling 1549 1559 */ 1550 1560 _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 ); 1561 var widgetControls = []; 1562 _.each( this.setting(), function ( widgetId ) { 1563 var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); 1564 if ( widgetControl ) { 1565 widgetControls.push( widgetControl ); 1566 } 1567 }); 1568 1569 if ( ! widgetControls.length ) { 1570 return; 1571 } 1572 1573 $( widgetControls ).each( function () { 1574 $( this.container ) 1575 .removeClass( 'first-widget' ) 1576 .removeClass( 'last-widget' ) 1577 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); 1578 }); 1555 1579 1556 this.$sectionContent.find( '.customize-control-widget_form:first' )1580 _.first( widgetControls ).container 1557 1581 .addClass( 'first-widget' ) 1558 1582 .find( '.move-widget-up' ).prop( 'tabIndex', -1 ); 1559 1583 1560 this.$sectionContent.find( '.customize-control-widget_form:last' )1584 _.last( widgetControls ).container 1561 1585 .addClass( 'last-widget' ) 1562 1586 .find( '.move-widget-down' ).prop( 'tabIndex', -1 ); 1563 1587 }, … … 1584 1608 1585 1609 if ( showOrHide ) { 1586 1610 _( this.getWidgetFormControls() ).each( function( formControl ) { 1587 formControl.collapse Form();1611 formControl.collapse(); 1588 1612 } ); 1589 1613 1590 1614 this.$sectionContent.find( '.first-widget .move-widget' ).focus(); … … 1651 1675 1652 1676 $widget = $( controlHtml ); 1653 1677 1678 // @todo need to pass this in as the control's 'content' property 1654 1679 $control = $( '<li/>' ) 1655 1680 .addClass( 'customize-control' ) 1656 1681 .addClass( 'customize-control-' + controlType ) … … 1674 1699 } 1675 1700 $control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); 1676 1701 1702 // @todo Eliminate this 1677 1703 this.container.after( $control ); 1678 1704 1679 1705 // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget) … … 1733 1759 1734 1760 $control.slideDown( function() { 1735 1761 if ( isExistingWidget ) { 1736 widgetFormControl.expand Form();1762 widgetFormControl.expand(); 1737 1763 widgetFormControl.updateWidget( { 1738 1764 instance: widgetFormControl.setting(), 1739 1765 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 7937d2d..129a85f 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; 221 223 $this->json['active'] = $this->active(); 224 $this->json['section'] = $this->section; 225 $this->json['content'] = $this->get_content(); 226 } 227 228 /** 229 * Get the data to export to the client via JSON. 230 * 231 * @since 4.1.0 232 * 233 * @return array 234 */ 235 public function json() { 236 $this->to_json(); 237 return $this->json; 222 238 } 223 239 224 240 /** … … class WP_Customize_Control { 242 258 } 243 259 244 260 /** 261 * Get the control's content for insertion into the Customizer pane. 262 * 263 * @since 4.1.0 264 * 265 * @return string 266 */ 267 public final function get_content() { 268 ob_start(); 269 $this->maybe_render(); 270 $template = trim( ob_get_contents() ); 271 ob_end_clean(); 272 return $template; 273 } 274 275 /** 245 276 * Check capabilities and render the control. 246 277 * 247 278 * @since 3.4.0 … … class WP_Customize_Header_Image_Control extends WP_Customize_Image_Control { 1031 1062 /** 1032 1063 * Widget Area Customize Control Class 1033 1064 * 1065 * @since 3.9.0 1034 1066 */ 1035 1067 class WP_Widget_Area_Customize_Control extends WP_Customize_Control { 1036 1068 public $type = 'sidebar_widgets'; … … class WP_Widget_Area_Customize_Control extends WP_Customize_Control { 1072 1104 1073 1105 /** 1074 1106 * Widget Form Customize Control Class 1107 * 1108 * @since 3.9.0 1075 1109 */ 1076 1110 class WP_Widget_Form_Customize_Control extends WP_Customize_Control { 1077 1111 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 febd8bc..ac70bdb 100644
final class WP_Customize_Manager { 491 491 $settings = array( 492 492 'values' => array(), 493 493 'channel' => wp_unslash( $_POST['customize_messenger_channel'] ), 494 'activePanels' => array(), 495 'activeSections' => array(), 494 496 'activeControls' => array(), 495 497 ); 496 498 … … final class WP_Customize_Manager { 504 506 foreach ( $this->settings as $id => $setting ) { 505 507 $settings['values'][ $id ] = $setting->js_value(); 506 508 } 509 foreach ( $this->panels as $id => $panel ) { 510 $settings['activePanels'][ $id ] = $panel->active(); 511 } 512 foreach ( $this->sections as $id => $section ) { 513 $settings['activeSections'][ $id ] = $section->active(); 514 } 507 515 foreach ( $this->controls as $id => $control ) { 508 516 $settings['activeControls'][ $id ] = $control->active(); 509 517 } … … final class WP_Customize_Manager { 878 886 879 887 if ( ! $section->panel ) { 880 888 // Top-level section. 881 $sections[ ] = $section;889 $sections[ $section->id ] = $section; 882 890 } else { 883 891 // This section belongs to a panel. 884 892 if ( isset( $this->panels [ $section->panel ] ) ) { 885 $this->panels[ $section->panel ]->sections[ ] = $section;893 $this->panels[ $section->panel ]->sections[ $section->id ] = $section; 886 894 } 887 895 } 888 896 } … … final class WP_Customize_Manager { 899 907 continue; 900 908 } 901 909 902 u sort( $panel->sections, array( $this, '_cmp_priority' ) );903 $panels[ ] = $panel;910 uasort( $panel->sections, array( $this, '_cmp_priority' ) ); 911 $panels[ $panel->id ] = $panel; 904 912 } 905 913 $this->panels = $panels; 906 914 -
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 });