Ticket #28709: 28709.9.diff
File 28709.9.diff, 66.8 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..eac27de 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() ) 258 254 ), 255 'autofocus' => array(), 259 256 ); 260 257 261 258 // Prepare Customize Setting objects to pass to Javascript. … … do_action( 'customize_controls_print_scripts' ); 266 263 ); 267 264 } 268 265 269 // Prepare Customize Control objects to pass to Java script.266 // Prepare Customize Control objects to pass to JavaScript. 270 267 foreach ( $wp_customize->controls() as $id => $control ) { 271 $control->to_json(); 272 $settings['controls'][ $id ] = $control->json; 268 $settings['controls'][ $id ] = $control->json(); 269 } 270 271 // Prepare Customize Section objects to pass to JavaScript. 272 foreach ( $wp_customize->sections() as $id => $section ) { 273 $settings['sections'][ $id ] = $section->json(); 274 } 275 276 // Prepare Customize Panel objects to pass to JavaScript. 277 foreach ( $wp_customize->panels() as $id => $panel ) { 278 $settings['panels'][ $id ] = $panel->json(); 279 foreach ( $panel->sections as $section_id => $section ) { 280 $settings['sections'][ $section_id ] = $section->json(); 281 } 282 } 283 284 // Pass to frontend the Customizer construct being deeplinked 285 if ( isset( $_GET['autofocus'] ) && is_array( $_GET['autofocus'] ) ) { 286 $autofocus = wp_unslash( $_GET['autofocus'] ); 287 foreach ( $autofocus as $type => $id ) { 288 if ( isset( $settings[ $type . 's' ][ $id ] ) ) { 289 $settings['autofocus'][ $type ] = $id; 290 } 291 } 273 292 } 274 293 275 294 ?> -
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..536e05a 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 container.container[0].scrollIntoView( true ); 61 }; 62 if ( params.completeCallback ) { 63 completeCallback = params.completeCallback; 64 params.completeCallback = function () { 65 focus(); 66 completeCallback(); 67 }; 68 } else { 69 params.completeCallback = focus; 70 } 71 if ( container.expand ) { 72 container.expand( params ); 73 } else { 74 params.completeCallback(); 75 } 76 }; 77 78 /** 79 * Base class for Panel and Section 80 * 34 81 * @constructor 35 82 * @augments wp.customize.Class 36 83 */ 37 api.Control = api.Class.extend({ 38 initialize: function( id, options ) { 39 var control = this, 40 nodes, radios, settings; 84 Container = api.Class.extend({ 85 defaultActiveArguments: { duration: 'fast' }, 86 defaultExpandedArguments: { duration: 'fast' }, 87 88 initialize: function ( id, options ) { 89 var container = this; 90 container.id = id; 91 container.params = {}; 92 $.extend( container, options || {} ); 93 container.container = $( container.params.content ); 94 95 container.deferred = { 96 ready: new $.Deferred() 97 }; 98 container.priority = new api.Value(); 99 container.active = new api.Value(); 100 container.activeArgumentsQueue = []; 101 container.expanded = new api.Value(); 102 container.expandedArgumentsQueue = []; 103 104 container.active.bind( function ( active ) { 105 var args = container.activeArgumentsQueue.shift(); 106 args = $.extend( {}, container.defaultActiveArguments, args ); 107 active = ( active && container.isContextuallyActive() ); 108 container.onChangeActive( active, args ); 109 // @todo trigger 'activated' and 'deactivated' events based on the expanded param? 110 }); 111 container.expanded.bind( function ( expanded ) { 112 var args = container.expandedArgumentsQueue.shift(); 113 args = $.extend( {}, container.defaultExpandedArguments, args ); 114 container.onChangeExpanded( expanded, args ); 115 // @todo trigger 'expanded' and 'collapsed' events based on the expanded param? 116 }); 41 117 42 this.params = {}; 43 $.extend( this, options || {} ); 118 container.attachEvents(); 44 119 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 ); 120 bubbleChildValueChanges( container, [ 'priority', 'active' ] ); 49 121 50 settings = $.map( this.params.settings, function( value ) { 51 return value; 122 container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority ); 123 container.active.set( container.params.active ); 124 container.expanded.set( false ); // @todo True if deeplinking? 125 }, 126 127 /** 128 * Get the child models associated with this parent, sorting them by their priority Value. 129 * 130 * @param {String} parentType 131 * @param {String} childType 132 * @returns {Array} 133 */ 134 _children: function ( parentType, childType ) { 135 var parent = this, 136 children = []; 137 api[ childType ].each( function ( child ) { 138 if ( child[ parentType ].get() === parent.id ) { 139 children.push( child ); 140 } 141 } ); 142 children.sort( function ( a, b ) { 143 return a.priority() - b.priority(); 144 } ); 145 return children; 146 }, 147 148 /** 149 * To override by subclass, to return whether the container has active children. 150 */ 151 isContextuallyActive: function () { 152 throw new Error( 'Must override with subclass.' ); 153 }, 154 155 /** 156 * Handle changes to the active state. 157 * This does not change the active state, it merely handles the behavior 158 * for when it does change. 159 * 160 * To override by subclass, update the container's UI to reflect the provided active state. 161 * 162 * @param {Boolean} active 163 * @param {Object} args merged on top of this.defaultActiveArguments 164 */ 165 onChangeActive: function ( active, args ) { 166 var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 ); 167 if ( active ) { 168 this.container.stop( true, true ).slideDown( duration, args.completeCallback ); 169 } else { 170 this.container.stop( true, true ).slideUp( duration, args.completeCallback ); 171 } 172 }, 173 174 /** 175 * @params {Boolean} active 176 * @param {Object} [params] 177 * @returns {Boolean} false if state already applied 178 */ 179 _toggleActive: function ( active, params ) { 180 var self = this; 181 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) { 182 setTimeout( function () { 183 self.onChangeActive( self.active.get(), params || {} ); 184 }); 185 return false; 186 } 187 this.activeArgumentsQueue.push( params || {} ); 188 this.active.set( active ); 189 return true; 190 }, 191 192 /** 193 * @param {Object} [params] 194 * @returns {Boolean} false if already active 195 */ 196 activate: function ( params ) { 197 return this._toggleActive( true, params ); 198 }, 199 200 /** 201 * @param {Object} [params] 202 * @returns {Boolean} false if already inactive 203 */ 204 deactivate: function ( params ) { 205 return this._toggleActive( false, params ); 206 }, 207 208 /** 209 * To override by subclass, update the container's UI to reflect the provided active state. 210 */ 211 onChangeExpanded: function () { 212 throw new Error( 'Must override with subclass.' ); 213 }, 214 215 /** 216 * @param {Boolean} expanded 217 * @param {Object} [params] 218 * @returns {Boolean} false if state already applied 219 */ 220 _toggleExpanded: function ( expanded, params ) { 221 var self = this; 222 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) { 223 setTimeout( function () { 224 self.onChangeExpanded( self.expanded.get(), params || {} ); 225 }); 226 return false; 227 } 228 this.expandedArgumentsQueue.push( params || {} ); 229 this.expanded.set( expanded ); 230 return true; 231 }, 232 233 /** 234 * @param {Object} [params] 235 * @returns {Boolean} false if already expanded 236 */ 237 expand: function ( params ) { 238 return this._toggleExpanded( true, params ); 239 }, 240 241 /** 242 * @param {Object} [params] 243 * @returns {Boolean} false if already collapsed 244 */ 245 collapse: function ( params ) { 246 return this._toggleExpanded( false, params ); 247 }, 248 249 /** 250 * Bring the container into view and then expand this and bring it into view 251 * @param {Object} [params] 252 */ 253 focus: focus 254 }); 255 256 /** 257 * @constructor 258 * @augments wp.customize.Class 259 */ 260 api.Section = Container.extend({ 261 262 /** 263 * @param {String} id 264 * @param {Array} options 265 */ 266 initialize: function ( id, options ) { 267 var section = this; 268 Container.prototype.initialize.call( section, id, options ); 269 270 section.panel = new api.Value(); 271 section.panel.bind( function ( id ) { 272 $( section.container ).toggleClass( 'control-subsection', !! id ); 52 273 }); 274 section.panel.set( section.params.panel || '' ); 275 bubbleChildValueChanges( section, [ 'panel' ] ); 53 276 54 api.apply( api, settings.concat( function() { 55 var key; 277 section.embed( function () { 278 section.deferred.ready.resolve(); 279 }); 280 }, 56 281 57 control.settings = {}; 58 for ( key in control.params.settings ) { 59 control.settings[ key ] = api( control.params.settings[ key ] ); 282 /** 283 * Embed the container in the DOM when any parent panel is ready. 284 * 285 * @param {Function} readyCallback 286 */ 287 embed: function ( readyCallback ) { 288 var panel_id = this.panel.get(), 289 section = this; 290 readyCallback = readyCallback || function () {}; 291 292 // Short-circuit if already embedded 293 if ( 'resolved' === section.deferred.ready.state() ) { 294 readyCallback(); 295 } else if ( ! panel_id ) { 296 $( '#customize-theme-controls > ul' ).append( section.container ); 297 readyCallback(); 298 } else { 299 api.panel( panel_id, function ( panel ) { 300 panel.deferred.ready.done( function () { 301 panel.container.find( 'ul:first' ).append( section.container ); 302 readyCallback(); 303 }); 304 }); 305 } 306 }, 307 308 /** 309 * Add behaviors for the accordion section 310 */ 311 attachEvents: function () { 312 var section = this; 313 314 // Expand/Collapse accordion sections on click. 315 section.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) { 316 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 317 return; 60 318 } 319 e.preventDefault(); // Keep this AFTER the key filter above 61 320 62 control.setting = control.settings['default'] || null; 63 control.renderContent( function() { 64 // Don't call ready() until the content has rendered. 65 control.ready(); 321 if ( section.expanded() ) { 322 section.collapse(); 323 } else { 324 section.expand(); 325 } 326 }); 327 }, 328 329 /** 330 * Return whether this section has any active controls. 331 * 332 * @returns {boolean} 333 */ 334 isContextuallyActive: function () { 335 var section = this, 336 controls = section.controls(), 337 activeCount = 0; 338 _( controls ).each( function ( control ) { 339 if ( control.active() ) { 340 activeCount += 1; 341 } 342 } ); 343 return ( activeCount !== 0 ); 344 }, 345 346 /** 347 * Get the controls that are associated with this section, sorted by their priority Value. 348 * 349 * @returns {Array} 350 */ 351 controls: function () { 352 return this._children( 'section', 'control' ); 353 }, 354 355 /** 356 * Update UI to reflect expanded state 357 * 358 * @param {Boolean} expanded 359 * @param {Object} args 360 */ 361 onChangeExpanded: function ( expanded, args ) { 362 var section = this, 363 content = section.container.find( '.accordion-section-content' ), 364 expand; 365 366 if ( expanded ) { 367 368 expand = function () { 369 content.stop().slideDown( args.duration, args.completeCallback ); 370 section.container.addClass( 'open' ); 371 }; 372 373 if ( ! args.allowMultiple ) { 374 api.section.each( function ( otherSection ) { 375 if ( otherSection !== section ) { 376 otherSection.collapse( {duration: 0} ); 377 } 378 }); 379 } 380 381 if ( section.panel() ) { 382 api.panel( section.panel() ).expand({ 383 duration: args.duration, 384 completeCallback: expand 385 }); 386 } else { 387 expand(); 388 } 389 390 } else { 391 section.container.removeClass( 'open' ); 392 content.slideUp( args.duration, args.completeCallback ); 393 } 394 } 395 }); 396 397 /** 398 * @constructor 399 * @augments wp.customize.Class 400 */ 401 api.Panel = Container.extend({ 402 initialize: function ( id, options ) { 403 var panel = this; 404 Container.prototype.initialize.call( panel, id, options ); 405 406 panel.embed( function () { 407 panel.deferred.ready.resolve(); 408 }); 409 }, 410 411 /** 412 * Embed the container in the DOM when any parent panel is ready. 413 * 414 * @param {Function} readyCallback 415 */ 416 embed: function ( readyCallback ) { 417 var panel = this; 418 readyCallback = readyCallback || function () {}; 419 420 // Short-circuit if already embedded 421 if ( 'resolved' === panel.deferred.ready.state() ) { 422 readyCallback(); 423 } else { 424 $( '#customize-theme-controls > ul' ).append( panel.container ); 425 readyCallback(); 426 } 427 }, 428 429 /** 430 * 431 */ 432 attachEvents: function () { 433 var meta, panel = this; 434 435 // Expand/Collapse accordion sections on click. 436 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( e ) { 437 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 438 return; 439 } 440 e.preventDefault(); // Keep this AFTER the key filter above 441 442 if ( ! panel.expanded() ) { 443 panel.expand(); 444 } 445 }); 446 447 meta = panel.container.find( '.panel-meta:first' ); 448 449 meta.find( '> .accordion-section-title' ).on( 'click keydown', function( e ) { 450 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 451 return; 452 } 453 e.preventDefault(); // Keep this AFTER the key filter above 454 455 if ( meta.hasClass( 'cannot-expand' ) ) { 456 return; 457 } 458 459 var content = meta.find( '.accordion-section-content:first' ); 460 if ( meta.hasClass( 'open' ) ) { 461 meta.toggleClass( 'open' ); 462 content.slideUp( 150 ); 463 } else { 464 content.slideDown( 150 ); 465 meta.toggleClass( 'open' ); 466 } 467 }); 468 469 }, 470 471 /** 472 * Get the sections that are associated with this panel, sorted by their priority Value. 473 * 474 * @returns {Array} 475 */ 476 sections: function () { 477 return this._children( 'panel', 'section' ); 478 }, 479 480 /** 481 * Return whether this panel has any active sections. 482 * 483 * @returns {boolean} 484 */ 485 isContextuallyActive: function () { 486 var panel = this, 487 sections = panel.sections(), 488 activeCount = 0; 489 _( sections ).each( function ( section ) { 490 if ( section.active() && section.isContextuallyActive() ) { 491 activeCount += 1; 492 } 493 } ); 494 return ( activeCount !== 0 ); 495 }, 496 497 /** 498 * Update UI to reflect expanded state 499 * 500 * @param {Boolean} expanded 501 * @param {Object} args merged with this.defaultExpandedArguments 502 */ 503 onChangeExpanded: function ( expanded, args ) { 504 505 // Note: there is a second argument 'args' passed 506 var position, scroll, 507 panel = this, 508 section = panel.container.closest( '.accordion-section' ), 509 overlay = section.closest( '.wp-full-overlay' ), 510 container = section.closest( '.accordion-container' ), 511 siblings = container.find( '.open' ), 512 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), 513 backBtn = overlay.find( '.control-panel-back' ), 514 panelTitle = section.find( '.accordion-section-title' ).first(), 515 content = section.find( '.control-panel-content' ); 516 517 if ( expanded ) { 518 519 // Collapse any sibling sections/panels 520 api.section.each( function ( section ) { 521 if ( ! section.panel() ) { 522 section.collapse( { duration: 0 } ); 523 } 524 }); 525 api.panel.each( function ( otherPanel ) { 526 if ( panel !== otherPanel ) { 527 otherPanel.collapse( { duration: 0 } ); 528 } 529 }); 530 531 content.show( 0, function() { 532 position = content.offset().top; 533 scroll = container.scrollTop(); 534 content.css( 'margin-top', ( 45 - position - scroll ) ); 535 section.addClass( 'current-panel' ); 536 overlay.addClass( 'in-sub-panel' ); 537 container.scrollTop( 0 ); 538 if ( args.completeCallback ) { 539 args.completeCallback(); 540 } 66 541 } ); 67 }) ); 542 topPanel.attr( 'tabindex', '-1' ); 543 backBtn.attr( 'tabindex', '0' ); 544 backBtn.focus(); 545 } else { 546 siblings.removeClass( 'open' ); 547 section.removeClass( 'current-panel' ); 548 overlay.removeClass( 'in-sub-panel' ); 549 content.delay( 180 ).hide( 0, function() { 550 content.css( 'margin-top', 'inherit' ); // Reset 551 if ( args.completeCallback ) { 552 args.completeCallback(); 553 } 554 } ); 555 topPanel.attr( 'tabindex', '0' ); 556 backBtn.attr( 'tabindex', '-1' ); 557 panelTitle.focus(); 558 container.scrollTop( 0 ); 559 } 560 } 561 }); 562 563 /** 564 * @constructor 565 * @augments wp.customize.Class 566 */ 567 api.Control = api.Class.extend({ 568 defaultActiveArguments: { duration: 'fast' }, 569 570 initialize: function( id, options ) { 571 var control = this, 572 nodes, radios, settings; 573 574 control.params = {}; 575 $.extend( control, options || {} ); 576 577 control.id = id; 578 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); 579 control.container = control.params.content ? $( control.params.content ) : $( control.selector ); 580 581 control.deferred = { 582 ready: new $.Deferred() 583 }; 584 control.section = new api.Value(); 585 control.priority = new api.Value(); 586 control.active = new api.Value(); 587 control.activeArgumentsQueue = []; 68 588 69 589 control.elements = []; 70 590 71 nodes = this.container.find('[data-customize-setting-link]');591 nodes = control.container.find('[data-customize-setting-link]'); 72 592 radios = {}; 73 593 74 594 nodes.each( function() { 75 var node = $( this),595 var node = $( this ), 76 596 name; 77 597 78 if ( node.is( ':radio') ) {79 name = node.prop( 'name');80 if ( radios[ name ] ) 598 if ( node.is( ':radio' ) ) { 599 name = node.prop( 'name' ); 600 if ( radios[ name ] ) { 81 601 return; 602 } 82 603 83 604 radios[ name ] = true; 84 605 node = nodes.filter( '[name="' + name + '"]' ); 85 606 } 86 607 87 api( node.data( 'customizeSettingLink'), function( setting ) {608 api( node.data( 'customizeSettingLink' ), function( setting ) { 88 609 var element = new api.Element( node ); 89 610 control.elements.push( element ); 90 611 element.sync( setting ); … … 93 614 }); 94 615 95 616 control.active.bind( function ( active ) { 96 control.toggle( active ); 617 var args = control.activeArgumentsQueue.shift(); 618 args = $.extend( {}, control.defaultActiveArguments, args ); 619 control.onChangeActive( active, args ); 620 } ); 621 622 control.section.set( control.params.section ); 623 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority ); 624 control.active.set( control.params.active ); 625 626 bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] ); 627 628 // Associate this control with its settings when they are created 629 settings = $.map( control.params.settings, function( value ) { 630 return value; 631 }); 632 api.apply( api, settings.concat( function () { 633 var key; 634 635 control.settings = {}; 636 for ( key in control.params.settings ) { 637 control.settings[ key ] = api( control.params.settings[ key ] ); 638 } 639 640 control.setting = control.settings['default'] || null; 641 control.embed( function () { 642 control.renderContent( function () { 643 // Don't call ready() until the content has rendered. 644 control.ready(); 645 control.deferred.ready.resolve(); 646 }); 647 }); 648 }) ); 649 }, 650 651 /** 652 * @param {Function} [readyCallback] Callback to fire when the embedding is done. 653 */ 654 embed: function ( readyCallback ) { 655 var section_id, 656 control = this; 657 readyCallback = readyCallback || function () {}; 658 659 // Short-circuit if already embedded 660 if ( 'resolved' === control.deferred.ready.state() ) { 661 readyCallback(); 662 return; 663 } 664 665 section_id = control.section.get(); 666 if ( ! section_id ) { 667 throw new Error( 'A control must have an associated section.' ); 668 // @todo Allow this to wait until control.section gets a value. Will require wp.customize.Value.once() 669 } 670 671 // Defer until the associated section is available 672 api.section( section_id, function ( section ) { 673 section.embed( function () { 674 section.container.find( 'ul:first' ).append( control.container ); 675 readyCallback(); 676 } ); 97 677 } ); 98 control.toggle( control.active() );99 678 }, 100 679 101 680 /** … … 104 683 ready: function() {}, 105 684 106 685 /** 107 * Callback for change to the control's active state. 108 * 109 * Override function for custom behavior for the control being active/inactive. 686 * Bring the containing section and panel into view and then this control into view, focusing on the first input 687 */ 688 focus: focus, 689 690 /** 691 * Update UI in response to a change in the control's active state. 692 * This does not change the active state, it merely handles the behavior 693 * for when it does change. 110 694 * 111 695 * @param {Boolean} active 696 * @param {Object} args merged on top of this.defaultActiveArguments 112 697 */ 113 toggle: function ( active) {698 onChangeActive: function ( active, args ) { 114 699 if ( active ) { 115 this.container.slideDown( );700 this.container.slideDown( args.duration ); 116 701 } else { 117 this.container.slideUp( );702 this.container.slideUp( args.duration ); 118 703 } 119 704 }, 120 705 706 /** 707 * @deprecated alias of onChangeActive 708 */ 709 toggle: function ( active ) { 710 return this.onChangeActive( active, this.defaultActiveArguments ); 711 }, 712 713 /** 714 * Shorthand way to enable the active state. 715 * 716 * @param {Object} [params] 717 * @returns {Boolean} false if already active 718 */ 719 activate: Container.prototype.activate, 720 721 /** 722 * Shorthand way to disable the active state. 723 * 724 * @param {Object} [params] 725 * @returns {Boolean} false if already inactive 726 */ 727 deactivate: Container.prototype.deactivate, 728 121 729 dropdownInit: function() { 122 730 var control = this, 123 731 statuses = this.container.find('.dropdown-status'), … … 158 766 * Render the control from its JS template, if it exists. 159 767 * 160 768 * The control's container must alreasy exist in the DOM. 769 * 770 * @param {Function} [callback] 161 771 */ 162 772 renderContent: function( callback ) { 163 773 var template, 164 selector = 'customize-control-' + this.params.type + '-content', 165 callback = callback || function(){}; 774 selector = 'customize-control-' + this.params.type + '-content'; 166 775 if ( 0 !== $( '#tmpl-' + selector ).length ) { 167 776 template = wp.template( selector ); 168 777 if ( template && this.container ) { 169 778 this.container.append( template( this.params ) ); 170 779 } 171 780 } 172 callback(); 781 if ( callback ) { 782 callback(); 783 } 173 784 } 174 785 }); 175 786 … … 596 1207 597 1208 // Create the collection of Control objects. 598 1209 api.control = new api.Values({ defaultConstructor: api.Control }); 1210 api.section = new api.Values({ defaultConstructor: api.Section }); 1211 api.panel = new api.Values({ defaultConstructor: api.Panel }); 599 1212 600 1213 /** 601 1214 * @constructor … … 631 1244 loaded = false, 632 1245 ready = false; 633 1246 634 if ( this._ready ) 1247 if ( this._ready ) { 635 1248 this.unbind( 'ready', this._ready ); 1249 } 636 1250 637 1251 this._ready = function() { 638 1252 ready = true; 639 1253 640 if ( loaded ) 1254 if ( loaded ) { 641 1255 deferred.resolveWith( self ); 1256 } 642 1257 }; 643 1258 644 1259 this.bind( 'ready', this._ready ); 645 1260 646 1261 this.bind( 'ready', function ( data ) { 647 if ( ! data || ! data.activeControls) {1262 if ( ! data ) { 648 1263 return; 649 1264 } 650 1265 651 $.each( data.activeControls, function ( id, active ) { 652 var control = api.control( id ); 653 if ( control ) { 654 control.active( active ); 1266 var constructs = { 1267 panel: data.activePanels, 1268 section: data.activeSections, 1269 control: data.activeControls 1270 }; 1271 1272 $.each( constructs, function ( type, activeConstructs ) { 1273 if ( activeConstructs ) { 1274 $.each( activeConstructs, function ( id, active ) { 1275 var construct = api[ type ]( id ); 1276 if ( construct ) { 1277 construct.active( active ); 1278 } 1279 } ); 655 1280 } 656 1281 } ); 1282 657 1283 } ); 658 1284 659 1285 this.request = $.ajax( this.previewUrl(), { … … 675 1301 676 1302 // Check if the location response header differs from the current URL. 677 1303 // If so, the request was redirected; try loading the requested page. 678 if ( location && location != self.previewUrl() ) {1304 if ( location && location !== self.previewUrl() ) { 679 1305 deferred.rejectWith( self, [ 'redirect', location ] ); 680 1306 return; 681 1307 } … … 802 1428 rscheme = /^https?/; 803 1429 804 1430 $.extend( this, options || {} ); 1431 this.deferred = { 1432 active: $.Deferred() 1433 }; 805 1434 806 1435 /* 807 1436 * Wrap this.refresh to prevent it from hammering the servers: … … 933 1562 self.targetWindow( this.targetWindow() ); 934 1563 self.channel( this.channel() ); 935 1564 1565 self.deferred.active.resolve(); 936 1566 self.send( 'active' ); 937 1567 }); 938 1568 … … 1000 1630 image: api.ImageControl, 1001 1631 header: api.HeaderControl 1002 1632 }; 1633 api.panelConstructor = {}; 1634 api.sectionConstructor = {}; 1003 1635 1004 1636 $( function() { 1005 1637 api.settings = window._wpCustomizeSettings; … … 1030 1662 } 1031 1663 }); 1032 1664 1665 // Expand/Collapse the main customizer customize info 1666 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( e ) { 1667 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 1668 return; 1669 } 1670 e.preventDefault(); // Keep this AFTER the key filter above 1671 1672 var section = $( this ).parent(), 1673 content = section.find( '.accordion-section-content:first' ); 1674 1675 if ( section.hasClass( 'cannot-expand' ) ) { 1676 return; 1677 } 1678 1679 if ( section.hasClass( 'open' ) ) { 1680 section.toggleClass( 'open' ); 1681 content.slideUp( 150 ); 1682 } else { 1683 content.slideDown( 150 ); 1684 section.toggleClass( 'open' ); 1685 } 1686 }); 1687 1033 1688 // Initialize Previewer 1034 1689 api.previewer = new api.Previewer({ 1035 1690 container: '#customize-preview', … … 1123 1778 $.extend( this.nonce, nonce ); 1124 1779 }); 1125 1780 1781 // Create Settings 1126 1782 $.each( api.settings.settings, function( id, data ) { 1127 1783 api.create( id, id, data.value, { 1128 1784 transport: data.transport, … … 1130 1786 } ); 1131 1787 }); 1132 1788 1789 // Create Panels 1790 $.each( api.settings.panels, function ( id, data ) { 1791 var constructor = api.panelConstructor[ data.type ] || api.Panel, 1792 panel; 1793 1794 panel = new constructor( id, { 1795 params: data 1796 } ); 1797 api.panel.add( id, panel ); 1798 }); 1799 1800 // Create Sections 1801 $.each( api.settings.sections, function ( id, data ) { 1802 var constructor = api.sectionConstructor[ data.type ] || api.Section, 1803 section; 1804 1805 section = new constructor( id, { 1806 params: data 1807 } ); 1808 api.section.add( id, section ); 1809 }); 1810 1811 // Create Controls 1133 1812 $.each( api.settings.controls, function( id, data ) { 1134 1813 var constructor = api.controlConstructor[ data.type ] || api.Control, 1135 1814 control; 1136 1815 1137 control = api.control.add( id,new constructor( id, {1816 control = new constructor( id, { 1138 1817 params: data, 1139 1818 previewer: api.previewer 1140 } ) ); 1819 } ); 1820 api.control.add( id, control ); 1821 }); 1822 1823 // Focus the autofocused element 1824 _.each( [ 'panel', 'section', 'control' ], function ( type ) { 1825 var instance, id = api.settings.autofocus[ type ]; 1826 if ( id && api[ type ]( id ) ) { 1827 instance = api[ type ]( id ); 1828 // Wait until the element is embedded in the DOM 1829 instance.deferred.ready.done( function () { 1830 // Wait until the preview has activated and so active panels, sections, controls have been set 1831 api.previewer.deferred.active.done( function () { 1832 instance.focus(); 1833 }); 1834 }); 1835 } 1141 1836 }); 1142 1837 1838 /** 1839 * Sort panels, sections, controls by priorities. Hide empty sections and panels. 1840 */ 1841 api.reflowPaneContents = _.bind( function () { 1842 1843 var appendContainer, activeElement, rootNodes = []; 1844 1845 if ( document.activeElement ) { 1846 activeElement = $( document.activeElement ); 1847 } 1848 1849 api.panel.each( function ( panel ) { 1850 var sections = panel.sections(); 1851 rootNodes.push( panel ); 1852 appendContainer = panel.container.find( 'ul:first' ); 1853 // @todo Skip doing any DOM manipulation if the ordering is already correct 1854 _( sections ).each( function ( section ) { 1855 appendContainer.append( section.container ); 1856 } ); 1857 } ); 1858 1859 api.section.each( function ( section ) { 1860 var controls = section.controls(); 1861 if ( ! section.panel() ) { 1862 rootNodes.push( section ); 1863 } 1864 appendContainer = section.container.find( 'ul:first' ); 1865 // @todo Skip doing any DOM manipulation if the ordering is already correct 1866 _( controls ).each( function ( control ) { 1867 appendContainer.append( control.container ); 1868 } ); 1869 } ); 1870 1871 // Sort the root elements 1872 rootNodes.sort( function ( a, b ) { 1873 return a.priority() - b.priority(); 1874 } ); 1875 appendContainer = $( '#customize-theme-controls > ul' ); 1876 // @todo Skip doing any DOM manipulation if the ordering is already correct 1877 _( rootNodes ).each( function ( rootNode ) { 1878 appendContainer.append( rootNode.container ); 1879 } ); 1880 1881 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered 1882 api.panel.each( function ( panel ) { 1883 var value = panel.active(); 1884 panel.active.callbacks.fireWith( panel.active, [ value, value ] ); 1885 } ); 1886 api.section.each( function ( section ) { 1887 var value = section.active(); 1888 section.active.callbacks.fireWith( section.active, [ value, value ] ); 1889 } ); 1890 1891 if ( activeElement ) { 1892 activeElement.focus(); 1893 } 1894 }, api ); 1895 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 ); 1896 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { 1897 values.bind( 'add', api.reflowPaneContents ); 1898 values.bind( 'change', api.reflowPaneContents ); 1899 values.bind( 'remove', api.reflowPaneContents ); 1900 } ); 1901 api.bind( 'ready', api.reflowPaneContents ); 1902 1143 1903 // Check if preview url is valid and load the preview frame. 1144 1904 if ( api.previewer.previewUrl() ) { 1145 1905 api.previewer.refresh(); … … 1204 1964 event.preventDefault(); 1205 1965 }); 1206 1966 1967 // Go back to the top-level Customizer accordion. 1968 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) { 1969 if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key 1970 return; 1971 } 1972 1973 e.preventDefault(); // Keep this AFTER the key filter above 1974 api.panel.each( function ( panel ) { 1975 panel.collapse(); 1976 }); 1977 }); 1978 1207 1979 closeBtn.keydown( function( event ) { 1208 1980 if ( 9 === event.which ) // tab 1209 1981 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 });