Changeset 30102
- Timestamp:
- 10/29/2014 10:50:21 PM (10 years ago)
- Location:
- trunk
- Files:
-
- 1 added
- 11 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-admin/customize.php
r30055 r30102 54 54 wp_enqueue_style( 'customize-controls' ); 55 55 56 wp_enqueue_script( 'accordion' );57 58 56 /** 59 57 * Enqueue Customizer control scripts. … … 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"> … … 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> … … 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 … … 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 -
trunk/src/wp-admin/js/accordion.js
r29610 r30102 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 */ … … 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 61 var sectionContent = $( '.accordion-section-content' );62 47 63 48 /** … … 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 return;77 }78 79 // Slide into a sub-panel instead of accordioning (Customizer-specific).80 if ( section.hasClass( 'control-panel' ) ) {81 panelSwitch( section );82 61 return; 83 62 } … … 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' ); … … 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); -
trunk/src/wp-admin/js/customize-controls.js
r30024 r30102 1 1 /* globals _wpCustomizeHeader, _wpMediaViewsL10n */ 2 2 (function( exports, $ ){ 3 var api = wp.customize; 3 var bubbleChildValueChanges, Container, focus, isKeydownButNotEnterEvent, areElementListsEqual, api = wp.customize; 4 5 // @todo Move private helper functions to wp.customize.utils so they can be unit tested 4 6 5 7 /** … … 32 34 33 35 /** 36 * Watch all changes to Value properties, and bubble changes to parent Values instance 37 * 38 * @param {wp.customize.Class} instance 39 * @param {Array} properties The names of the Value instances to watch. 40 */ 41 bubbleChildValueChanges = function ( instance, properties ) { 42 $.each( properties, function ( i, key ) { 43 instance[ key ].bind( function ( to, from ) { 44 if ( instance.parent && to !== from ) { 45 instance.parent.trigger( 'change', instance ); 46 } 47 } ); 48 } ); 49 }; 50 51 /** 52 * Expand a panel, section, or control and focus on the first focusable element. 53 * 54 * @param {Object} [params] 55 */ 56 focus = function ( params ) { 57 var construct, completeCallback, focus; 58 construct = this; 59 params = params || {}; 60 focus = function () { 61 construct.container.find( ':focusable:first' ).focus(); 62 construct.container[0].scrollIntoView( true ); 63 }; 64 if ( params.completeCallback ) { 65 completeCallback = params.completeCallback; 66 params.completeCallback = function () { 67 focus(); 68 completeCallback(); 69 }; 70 } else { 71 params.completeCallback = focus; 72 } 73 if ( construct.expand ) { 74 construct.expand( params ); 75 } else { 76 params.completeCallback(); 77 } 78 }; 79 80 /** 81 * Return whether the supplied Event object is for a keydown event but not the Enter key. 82 * 83 * @param {jQuery.Event} event 84 * @returns {boolean} 85 */ 86 isKeydownButNotEnterEvent = function ( event ) { 87 return ( 'keydown' === event.type && 13 !== event.which ); 88 }; 89 90 /** 91 * Return whether the two lists of elements are the same and are in the same order. 92 * 93 * @param {Array|jQuery} listA 94 * @param {Array|jQuery} listB 95 * @returns {boolean} 96 */ 97 areElementListsEqual = function ( listA, listB ) { 98 var equal = ( 99 listA.length === listB.length && // if lists are different lengths, then naturally they are not equal 100 -1 === _.map( // are there any false values in the list returned by map? 101 _.zip( listA, listB ), // pair up each element between the two lists 102 function ( pair ) { 103 return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal 104 } 105 ).indexOf( false ) // check for presence of false in map's return value 106 ); 107 return equal; 108 }; 109 110 /** 111 * Base class for Panel and Section 112 * 113 * @constructor 114 * @augments wp.customize.Class 115 */ 116 Container = api.Class.extend({ 117 defaultActiveArguments: { duration: 'fast' }, 118 defaultExpandedArguments: { duration: 'fast' }, 119 120 initialize: function ( id, options ) { 121 var container = this; 122 container.id = id; 123 container.params = {}; 124 $.extend( container, options || {} ); 125 container.container = $( container.params.content ); 126 127 container.deferred = { 128 ready: new $.Deferred() 129 }; 130 container.priority = new api.Value(); 131 container.active = new api.Value(); 132 container.activeArgumentsQueue = []; 133 container.expanded = new api.Value(); 134 container.expandedArgumentsQueue = []; 135 136 container.active.bind( function ( active ) { 137 var args = container.activeArgumentsQueue.shift(); 138 args = $.extend( {}, container.defaultActiveArguments, args ); 139 active = ( active && container.isContextuallyActive() ); 140 container.onChangeActive( active, args ); 141 // @todo trigger 'activated' and 'deactivated' events based on the expanded param? 142 }); 143 container.expanded.bind( function ( expanded ) { 144 var args = container.expandedArgumentsQueue.shift(); 145 args = $.extend( {}, container.defaultExpandedArguments, args ); 146 container.onChangeExpanded( expanded, args ); 147 // @todo trigger 'expanded' and 'collapsed' events based on the expanded param? 148 }); 149 150 container.attachEvents(); 151 152 bubbleChildValueChanges( container, [ 'priority', 'active' ] ); 153 154 container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority ); 155 container.active.set( container.params.active ); 156 container.expanded.set( false ); // @todo True if deeplinking? 157 }, 158 159 /** 160 * @abstract 161 */ 162 ready: function() {}, 163 164 /** 165 * Get the child models associated with this parent, sorting them by their priority Value. 166 * 167 * @param {String} parentType 168 * @param {String} childType 169 * @returns {Array} 170 */ 171 _children: function ( parentType, childType ) { 172 var parent = this, 173 children = []; 174 api[ childType ].each( function ( child ) { 175 if ( child[ parentType ].get() === parent.id ) { 176 children.push( child ); 177 } 178 } ); 179 children.sort( function ( a, b ) { 180 return a.priority() - b.priority(); 181 } ); 182 return children; 183 }, 184 185 /** 186 * To override by subclass, to return whether the container has active children. 187 * @abstract 188 */ 189 isContextuallyActive: function () { 190 throw new Error( 'Must override with subclass.' ); 191 }, 192 193 /** 194 * Handle changes to the active state. 195 * This does not change the active state, it merely handles the behavior 196 * for when it does change. 197 * 198 * To override by subclass, update the container's UI to reflect the provided active state. 199 * 200 * @param {Boolean} active 201 * @param {Object} args merged on top of this.defaultActiveArguments 202 */ 203 onChangeActive: function ( active, args ) { 204 var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 ); 205 if ( active ) { 206 this.container.stop( true, true ).slideDown( duration, args.completeCallback ); 207 } else { 208 this.container.stop( true, true ).slideUp( duration, args.completeCallback ); 209 } 210 }, 211 212 /** 213 * @params {Boolean} active 214 * @param {Object} [params] 215 * @returns {Boolean} false if state already applied 216 */ 217 _toggleActive: function ( active, params ) { 218 var self = this; 219 params = params || {}; 220 if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) { 221 params.unchanged = true; 222 self.onChangeActive( self.active.get(), params ); 223 return false; 224 } else { 225 params.unchanged = false; 226 this.activeArgumentsQueue.push( params ); 227 this.active.set( active ); 228 return true; 229 } 230 }, 231 232 /** 233 * @param {Object} [params] 234 * @returns {Boolean} false if already active 235 */ 236 activate: function ( params ) { 237 return this._toggleActive( true, params ); 238 }, 239 240 /** 241 * @param {Object} [params] 242 * @returns {Boolean} false if already inactive 243 */ 244 deactivate: function ( params ) { 245 return this._toggleActive( false, params ); 246 }, 247 248 /** 249 * To override by subclass, update the container's UI to reflect the provided active state. 250 * @abstract 251 */ 252 onChangeExpanded: function () { 253 throw new Error( 'Must override with subclass.' ); 254 }, 255 256 /** 257 * @param {Boolean} expanded 258 * @param {Object} [params] 259 * @returns {Boolean} false if state already applied 260 */ 261 _toggleExpanded: function ( expanded, params ) { 262 var self = this; 263 params = params || {}; 264 if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) { 265 params.unchanged = true; 266 self.onChangeExpanded( self.expanded.get(), params ); 267 return false; 268 } else { 269 params.unchanged = false; 270 this.expandedArgumentsQueue.push( params ); 271 this.expanded.set( expanded ); 272 return true; 273 } 274 }, 275 276 /** 277 * @param {Object} [params] 278 * @returns {Boolean} false if already expanded 279 */ 280 expand: function ( params ) { 281 return this._toggleExpanded( true, params ); 282 }, 283 284 /** 285 * @param {Object} [params] 286 * @returns {Boolean} false if already collapsed 287 */ 288 collapse: function ( params ) { 289 return this._toggleExpanded( false, params ); 290 }, 291 292 /** 293 * Bring the container into view and then expand this and bring it into view 294 * @param {Object} [params] 295 */ 296 focus: focus 297 }); 298 299 /** 300 * @constructor 301 * @augments wp.customize.Class 302 */ 303 api.Section = Container.extend({ 304 305 /** 306 * @param {String} id 307 * @param {Array} options 308 */ 309 initialize: function ( id, options ) { 310 var section = this; 311 Container.prototype.initialize.call( section, id, options ); 312 313 section.id = id; 314 section.panel = new api.Value(); 315 section.panel.bind( function ( id ) { 316 $( section.container ).toggleClass( 'control-subsection', !! id ); 317 }); 318 section.panel.set( section.params.panel || '' ); 319 bubbleChildValueChanges( section, [ 'panel' ] ); 320 321 section.embed(); 322 section.deferred.ready.done( function () { 323 section.ready(); 324 }); 325 }, 326 327 /** 328 * Embed the container in the DOM when any parent panel is ready. 329 */ 330 embed: function () { 331 var section = this, inject; 332 333 // Watch for changes to the panel state 334 inject = function ( panelId ) { 335 var parentContainer; 336 if ( panelId ) { 337 // The panel has been supplied, so wait until the panel object is registered 338 api.panel( panelId, function ( panel ) { 339 // The panel has been registered, wait for it to become ready/initialized 340 panel.deferred.ready.done( function () { 341 parentContainer = panel.container.find( 'ul:first' ); 342 if ( ! section.container.parent().is( parentContainer ) ) { 343 parentContainer.append( section.container ); 344 } 345 section.deferred.ready.resolve(); // @todo Better to use `embedded` instead of `ready` 346 }); 347 } ); 348 } else { 349 // There is no panel, so embed the section in the root of the customizer 350 parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable 351 if ( ! section.container.parent().is( parentContainer ) ) { 352 parentContainer.append( section.container ); 353 } 354 section.deferred.ready.resolve(); 355 } 356 }; 357 section.panel.bind( inject ); 358 inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one 359 }, 360 361 /** 362 * Add behaviors for the accordion section 363 */ 364 attachEvents: function () { 365 var section = this; 366 367 // Expand/Collapse accordion sections on click. 368 section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) { 369 if ( isKeydownButNotEnterEvent( event ) ) { 370 return; 371 } 372 event.preventDefault(); // Keep this AFTER the key filter above 373 374 if ( section.expanded() ) { 375 section.collapse(); 376 } else { 377 section.expand(); 378 } 379 }); 380 }, 381 382 /** 383 * Return whether this section has any active controls. 384 * 385 * @returns {boolean} 386 */ 387 isContextuallyActive: function () { 388 var section = this, 389 controls = section.controls(), 390 activeCount = 0; 391 _( controls ).each( function ( control ) { 392 if ( control.active() ) { 393 activeCount += 1; 394 } 395 } ); 396 return ( activeCount !== 0 ); 397 }, 398 399 /** 400 * Get the controls that are associated with this section, sorted by their priority Value. 401 * 402 * @returns {Array} 403 */ 404 controls: function () { 405 return this._children( 'section', 'control' ); 406 }, 407 408 /** 409 * Update UI to reflect expanded state 410 * 411 * @param {Boolean} expanded 412 * @param {Object} args 413 */ 414 onChangeExpanded: function ( expanded, args ) { 415 var section = this, 416 content = section.container.find( '.accordion-section-content' ), 417 expand; 418 419 if ( expanded ) { 420 421 if ( args.unchanged ) { 422 expand = args.completeCallback; 423 } else { 424 expand = function () { 425 content.stop().slideDown( args.duration, args.completeCallback ); 426 section.container.addClass( 'open' ); 427 }; 428 } 429 430 if ( ! args.allowMultiple ) { 431 api.section.each( function ( otherSection ) { 432 if ( otherSection !== section ) { 433 otherSection.collapse( { duration: args.duration } ); 434 } 435 }); 436 } 437 438 if ( section.panel() ) { 439 api.panel( section.panel() ).expand({ 440 duration: args.duration, 441 completeCallback: expand 442 }); 443 } else { 444 expand(); 445 } 446 447 } else { 448 section.container.removeClass( 'open' ); 449 content.slideUp( args.duration, args.completeCallback ); 450 } 451 } 452 }); 453 454 /** 455 * @constructor 456 * @augments wp.customize.Class 457 */ 458 api.Panel = Container.extend({ 459 initialize: function ( id, options ) { 460 var panel = this; 461 Container.prototype.initialize.call( panel, id, options ); 462 panel.embed(); 463 panel.deferred.ready.done( function () { 464 panel.ready(); 465 }); 466 }, 467 468 /** 469 * Embed the container in the DOM when any parent panel is ready. 470 */ 471 embed: function () { 472 var panel = this, 473 parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable 474 475 if ( ! panel.container.parent().is( parentContainer ) ) { 476 parentContainer.append( panel.container ); 477 } 478 panel.deferred.ready.resolve(); 479 }, 480 481 /** 482 * 483 */ 484 attachEvents: function () { 485 var meta, panel = this; 486 487 // Expand/Collapse accordion sections on click. 488 panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) { 489 if ( isKeydownButNotEnterEvent( event ) ) { 490 return; 491 } 492 event.preventDefault(); // Keep this AFTER the key filter above 493 494 if ( ! panel.expanded() ) { 495 panel.expand(); 496 } 497 }); 498 499 meta = panel.container.find( '.panel-meta:first' ); 500 501 meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) { 502 if ( isKeydownButNotEnterEvent( event ) ) { 503 return; 504 } 505 event.preventDefault(); // Keep this AFTER the key filter above 506 507 if ( meta.hasClass( 'cannot-expand' ) ) { 508 return; 509 } 510 511 var content = meta.find( '.accordion-section-content:first' ); 512 if ( meta.hasClass( 'open' ) ) { 513 meta.toggleClass( 'open' ); 514 content.slideUp( panel.defaultExpandedArguments.duration ); 515 } else { 516 content.slideDown( panel.defaultExpandedArguments.duration ); 517 meta.toggleClass( 'open' ); 518 } 519 }); 520 521 }, 522 523 /** 524 * Get the sections that are associated with this panel, sorted by their priority Value. 525 * 526 * @returns {Array} 527 */ 528 sections: function () { 529 return this._children( 'panel', 'section' ); 530 }, 531 532 /** 533 * Return whether this panel has any active sections. 534 * 535 * @returns {boolean} 536 */ 537 isContextuallyActive: function () { 538 var panel = this, 539 sections = panel.sections(), 540 activeCount = 0; 541 _( sections ).each( function ( section ) { 542 if ( section.active() && section.isContextuallyActive() ) { 543 activeCount += 1; 544 } 545 } ); 546 return ( activeCount !== 0 ); 547 }, 548 549 /** 550 * Update UI to reflect expanded state 551 * 552 * @param {Boolean} expanded 553 * @param {Object} args merged with this.defaultExpandedArguments 554 */ 555 onChangeExpanded: function ( expanded, args ) { 556 557 // Immediately call the complete callback if there were no changes 558 if ( args.unchanged ) { 559 if ( args.completeCallback ) { 560 args.completeCallback(); 561 } 562 return; 563 } 564 565 // Note: there is a second argument 'args' passed 566 var position, scroll, 567 panel = this, 568 section = panel.container.closest( '.accordion-section' ), 569 overlay = section.closest( '.wp-full-overlay' ), 570 container = section.closest( '.accordion-container' ), 571 siblings = container.find( '.open' ), 572 topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), 573 backBtn = overlay.find( '.control-panel-back' ), 574 panelTitle = section.find( '.accordion-section-title' ).first(), 575 content = section.find( '.control-panel-content' ); 576 577 if ( expanded ) { 578 579 // Collapse any sibling sections/panels 580 api.section.each( function ( section ) { 581 if ( ! section.panel() ) { 582 section.collapse( { duration: 0 } ); 583 } 584 }); 585 api.panel.each( function ( otherPanel ) { 586 if ( panel !== otherPanel ) { 587 otherPanel.collapse( { duration: 0 } ); 588 } 589 }); 590 591 content.show( 0, function() { 592 position = content.offset().top; 593 scroll = container.scrollTop(); 594 content.css( 'margin-top', ( 45 - position - scroll ) ); 595 section.addClass( 'current-panel' ); 596 overlay.addClass( 'in-sub-panel' ); 597 container.scrollTop( 0 ); 598 if ( args.completeCallback ) { 599 args.completeCallback(); 600 } 601 } ); 602 topPanel.attr( 'tabindex', '-1' ); 603 backBtn.attr( 'tabindex', '0' ); 604 backBtn.focus(); 605 } else { 606 siblings.removeClass( 'open' ); 607 section.removeClass( 'current-panel' ); 608 overlay.removeClass( 'in-sub-panel' ); 609 content.delay( 180 ).hide( 0, function() { 610 content.css( 'margin-top', 'inherit' ); // Reset 611 if ( args.completeCallback ) { 612 args.completeCallback(); 613 } 614 } ); 615 topPanel.attr( 'tabindex', '0' ); 616 backBtn.attr( 'tabindex', '-1' ); 617 panelTitle.focus(); 618 container.scrollTop( 0 ); 619 } 620 } 621 }); 622 623 /** 34 624 * @constructor 35 625 * @augments wp.customize.Class 36 626 */ 37 627 api.Control = api.Class.extend({ 628 defaultActiveArguments: { duration: 'fast' }, 629 38 630 initialize: function( id, options ) { 39 631 var control = this, 40 632 nodes, radios, settings; 41 633 42 this.params = {}; 43 $.extend( this, options || {} ); 44 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 ); 49 50 settings = $.map( this.params.settings, function( value ) { 51 return value; 52 }); 53 54 api.apply( api, settings.concat( function() { 55 var key; 56 57 control.settings = {}; 58 for ( key in control.params.settings ) { 59 control.settings[ key ] = api( control.params.settings[ key ] ); 60 } 61 62 control.setting = control.settings['default'] || null; 63 control.renderContent( function() { 64 // Don't call ready() until the content has rendered. 65 control.ready(); 66 } ); 67 }) ); 634 control.params = {}; 635 $.extend( control, options || {} ); 636 637 control.id = id; 638 control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); 639 control.templateSelector = 'customize-control-' + control.params.type + '-content'; 640 control.container = control.params.content ? $( control.params.content ) : $( control.selector ); 641 642 control.deferred = { 643 ready: new $.Deferred() 644 }; 645 control.section = new api.Value(); 646 control.priority = new api.Value(); 647 control.active = new api.Value(); 648 control.activeArgumentsQueue = []; 68 649 69 650 control.elements = []; 70 651 71 nodes = this.container.find('[data-customize-setting-link]');652 nodes = control.container.find('[data-customize-setting-link]'); 72 653 radios = {}; 73 654 74 655 nodes.each( function() { 75 var node = $( this),656 var node = $( this ), 76 657 name; 77 658 78 if ( node.is( ':radio') ) {79 name = node.prop( 'name');80 if ( radios[ name ] ) 659 if ( node.is( ':radio' ) ) { 660 name = node.prop( 'name' ); 661 if ( radios[ name ] ) { 81 662 return; 663 } 82 664 83 665 radios[ name ] = true; … … 85 667 } 86 668 87 api( node.data( 'customizeSettingLink'), function( setting ) {669 api( node.data( 'customizeSettingLink' ), function( setting ) { 88 670 var element = new api.Element( node ); 89 671 control.elements.push( element ); … … 94 676 95 677 control.active.bind( function ( active ) { 96 control.toggle( active ); 678 var args = control.activeArgumentsQueue.shift(); 679 args = $.extend( {}, control.defaultActiveArguments, args ); 680 control.onChangeActive( active, args ); 97 681 } ); 98 control.toggle( control.active() ); 682 683 control.section.set( control.params.section ); 684 control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority ); 685 control.active.set( control.params.active ); 686 687 bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] ); 688 689 // Associate this control with its settings when they are created 690 settings = $.map( control.params.settings, function( value ) { 691 return value; 692 }); 693 api.apply( api, settings.concat( function () { 694 var key; 695 696 control.settings = {}; 697 for ( key in control.params.settings ) { 698 control.settings[ key ] = api( control.params.settings[ key ] ); 699 } 700 701 control.setting = control.settings['default'] || null; 702 703 control.embed(); 704 }) ); 705 706 control.deferred.ready.done( function () { 707 control.ready(); 708 }); 709 }, 710 711 /** 712 * 713 */ 714 embed: function () { 715 var control = this, 716 inject; 717 718 // Watch for changes to the section state 719 inject = function ( sectionId ) { 720 var parentContainer; 721 if ( ! sectionId ) { // @todo allow a control to be embeded without a section, for instance a control embedded in the frontend 722 return; 723 } 724 // Wait for the section to be registered 725 api.section( sectionId, function ( section ) { 726 // Wait for the section to be ready/initialized 727 section.deferred.ready.done( function () { 728 parentContainer = section.container.find( 'ul:first' ); 729 if ( ! control.container.parent().is( parentContainer ) ) { 730 parentContainer.append( control.container ); 731 control.renderContent(); 732 } 733 control.deferred.ready.resolve(); // @todo Better to use `embedded` instead of `ready` 734 }); 735 }); 736 }; 737 control.section.bind( inject ); 738 inject( control.section.get() ); 99 739 }, 100 740 … … 105 745 106 746 /** 107 * Callback for change to the control's active state.747 * Normal controls do not expand, so just expand its parent 108 748 * 109 * Override function for custom behavior for the control being active/inactive. 749 * @param {Object} [params] 750 */ 751 expand: function ( params ) { 752 api.section( this.section() ).expand( params ); 753 }, 754 755 /** 756 * Bring the containing section and panel into view and then this control into view, focusing on the first input 757 */ 758 focus: focus, 759 760 /** 761 * Update UI in response to a change in the control's active state. 762 * This does not change the active state, it merely handles the behavior 763 * for when it does change. 110 764 * 111 765 * @param {Boolean} active 766 * @param {Object} args merged on top of this.defaultActiveArguments 767 */ 768 onChangeActive: function ( active, args ) { 769 if ( active ) { 770 this.container.slideDown( args.duration, args.completeCallback ); 771 } else { 772 this.container.slideUp( args.duration, args.completeCallback ); 773 } 774 }, 775 776 /** 777 * @deprecated alias of onChangeActive 112 778 */ 113 779 toggle: function ( active ) { 114 if ( active ) { 115 this.container.slideDown(); 116 } else { 117 this.container.slideUp(); 118 } 119 }, 780 return this.onChangeActive( active, this.defaultActiveArguments ); 781 }, 782 783 /** 784 * Shorthand way to enable the active state. 785 * 786 * @param {Object} [params] 787 * @returns {Boolean} false if already active 788 */ 789 activate: Container.prototype.activate, 790 791 /** 792 * Shorthand way to disable the active state. 793 * 794 * @param {Object} [params] 795 * @returns {Boolean} false if already inactive 796 */ 797 deactivate: Container.prototype.deactivate, 120 798 121 799 dropdownInit: function() { … … 133 811 // Support the .dropdown class to open/close complex elements 134 812 this.container.on( 'click keydown', '.dropdown', function( event ) { 135 if ( event.type === 'keydown' && 13 !== event.which ) // enter813 if ( isKeydownButNotEnterEvent( event ) ) { 136 814 return; 815 } 137 816 138 817 event.preventDefault(); … … 158 837 * Render the control from its JS template, if it exists. 159 838 * 160 * The control's container must alrea sy exist in the DOM.161 */ 162 renderContent: function ( callback) {839 * The control's container must already exist in the DOM. 840 */ 841 renderContent: function () { 163 842 var template, 164 selector = 'customize-control-' + this.params.type + '-content'; 165 166 callback = callback || function(){}; 167 if ( 0 !== $( '#tmpl-' + selector ).length ) { 168 template = wp.template( selector ); 169 if ( template && this.container ) { 170 this.container.append( template( this.params ) ); 171 } 172 } 173 callback(); 843 control = this; 844 845 if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) { 846 template = wp.template( control.templateSelector ); 847 if ( template && control.container ) { 848 control.container.append( template( control.params ) ); 849 } 850 } 174 851 } 175 852 }); … … 235 912 this.remover = this.container.find('.remove'); 236 913 this.remover.on( 'click keydown', function( event ) { 237 if ( event.type === 'keydown' && 13 !== event.which ) // enter914 if ( isKeydownButNotEnterEvent( event ) ) { 238 915 return; 916 } 239 917 240 918 control.setting.set( control.params.removed ); … … 307 985 // Bind tab switch events 308 986 this.library.children('ul').on( 'click keydown', 'li', function( event ) { 309 if ( event.type === 'keydown' && 13 !== event.which ) // enter987 if ( isKeydownButNotEnterEvent( event ) ) { 310 988 return; 989 } 311 990 312 991 var id = $(this).data('customizeTab'), … … 325 1004 // Bind events to switch image urls. 326 1005 this.library.on( 'click keydown', 'a', function( event ) { 327 if ( event.type === 'keydown' && 13 !== event.which ) // enter1006 if ( isKeydownButNotEnterEvent( event ) ) { 328 1007 return; 1008 } 329 1009 330 1010 var value = $(this).data('customizeImageValue'); … … 598 1278 // Create the collection of Control objects. 599 1279 api.control = new api.Values({ defaultConstructor: api.Control }); 1280 api.section = new api.Values({ defaultConstructor: api.Section }); 1281 api.panel = new api.Values({ defaultConstructor: api.Panel }); 600 1282 601 1283 /** … … 633 1315 ready = false; 634 1316 635 if ( this._ready ) 1317 if ( this._ready ) { 636 1318 this.unbind( 'ready', this._ready ); 1319 } 637 1320 638 1321 this._ready = function() { 639 1322 ready = true; 640 1323 641 if ( loaded ) 1324 if ( loaded ) { 642 1325 deferred.resolveWith( self ); 1326 } 643 1327 }; 644 1328 … … 646 1330 647 1331 this.bind( 'ready', function ( data ) { 648 if ( ! data || ! data.activeControls) {1332 if ( ! data ) { 649 1333 return; 650 1334 } 651 1335 652 $.each( data.activeControls, function ( id, active ) { 653 var control = api.control( id ); 654 if ( control ) { 655 control.active( active ); 1336 var constructs = { 1337 panel: data.activePanels, 1338 section: data.activeSections, 1339 control: data.activeControls 1340 }; 1341 1342 $.each( constructs, function ( type, activeConstructs ) { 1343 if ( activeConstructs ) { 1344 $.each( activeConstructs, function ( id, active ) { 1345 var construct = api[ type ]( id ); 1346 if ( construct ) { 1347 construct.active( active ); 1348 } 1349 } ); 656 1350 } 657 1351 } ); 1352 658 1353 } ); 659 1354 … … 677 1372 // Check if the location response header differs from the current URL. 678 1373 // If so, the request was redirected; try loading the requested page. 679 if ( location && location != self.previewUrl() ) {1374 if ( location && location !== self.previewUrl() ) { 680 1375 deferred.rejectWith( self, [ 'redirect', location ] ); 681 1376 return; … … 804 1499 805 1500 $.extend( this, options || {} ); 1501 this.deferred = { 1502 active: $.Deferred() 1503 }; 806 1504 807 1505 /* … … 935 1633 self.channel( this.channel() ); 936 1634 1635 self.deferred.active.resolve(); 937 1636 self.send( 'active' ); 938 1637 }); … … 1002 1701 header: api.HeaderControl 1003 1702 }; 1703 api.panelConstructor = {}; 1704 api.sectionConstructor = {}; 1004 1705 1005 1706 $( function() { … … 1029 1730 if ( isEnter && ( $el.is( 'input:not([type=button])' ) || $el.is( 'select' ) ) ) { 1030 1731 e.preventDefault(); 1732 } 1733 }); 1734 1735 // Expand/Collapse the main customizer customize info 1736 $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) { 1737 if ( isKeydownButNotEnterEvent( event ) ) { 1738 return; 1739 } 1740 event.preventDefault(); // Keep this AFTER the key filter above 1741 1742 var section = $( this ).parent(), 1743 content = section.find( '.accordion-section-content:first' ); 1744 1745 if ( section.hasClass( 'cannot-expand' ) ) { 1746 return; 1747 } 1748 1749 if ( section.hasClass( 'open' ) ) { 1750 section.toggleClass( 'open' ); 1751 content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration ); 1752 } else { 1753 content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration ); 1754 section.toggleClass( 'open' ); 1031 1755 } 1032 1756 }); … … 1125 1849 }); 1126 1850 1851 // Create Settings 1127 1852 $.each( api.settings.settings, function( id, data ) { 1128 1853 api.create( id, id, data.value, { … … 1132 1857 }); 1133 1858 1859 // Create Panels 1860 $.each( api.settings.panels, function ( id, data ) { 1861 var constructor = api.panelConstructor[ data.type ] || api.Panel, 1862 panel; 1863 1864 panel = new constructor( id, { 1865 params: data 1866 } ); 1867 api.panel.add( id, panel ); 1868 }); 1869 1870 // Create Sections 1871 $.each( api.settings.sections, function ( id, data ) { 1872 var constructor = api.sectionConstructor[ data.type ] || api.Section, 1873 section; 1874 1875 section = new constructor( id, { 1876 params: data 1877 } ); 1878 api.section.add( id, section ); 1879 }); 1880 1881 // Create Controls 1134 1882 $.each( api.settings.controls, function( id, data ) { 1135 1883 var constructor = api.controlConstructor[ data.type ] || api.Control, 1136 1884 control; 1137 1885 1138 control = api.control.add( id,new constructor( id, {1886 control = new constructor( id, { 1139 1887 params: data, 1140 1888 previewer: api.previewer 1141 } ) ); 1889 } ); 1890 api.control.add( id, control ); 1142 1891 }); 1892 1893 // Focus the autofocused element 1894 _.each( [ 'panel', 'section', 'control' ], function ( type ) { 1895 var instance, id = api.settings.autofocus[ type ]; 1896 if ( id && api[ type ]( id ) ) { 1897 instance = api[ type ]( id ); 1898 // Wait until the element is embedded in the DOM 1899 instance.deferred.ready.done( function () { 1900 // Wait until the preview has activated and so active panels, sections, controls have been set 1901 api.previewer.deferred.active.done( function () { 1902 instance.focus(); 1903 }); 1904 }); 1905 } 1906 }); 1907 1908 /** 1909 * Sort panels, sections, controls by priorities. Hide empty sections and panels. 1910 */ 1911 api.reflowPaneContents = _.bind( function () { 1912 1913 var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false; 1914 1915 if ( document.activeElement ) { 1916 activeElement = $( document.activeElement ); 1917 } 1918 1919 // Sort the sections within each panel 1920 api.panel.each( function ( panel ) { 1921 var sections = panel.sections(), 1922 sectionContainers = _.pluck( sections, 'container' ); 1923 rootNodes.push( panel ); 1924 appendContainer = panel.container.find( 'ul:first' ); 1925 if ( ! areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) { 1926 _( sections ).each( function ( section ) { 1927 appendContainer.append( section.container ); 1928 } ); 1929 wasReflowed = true; 1930 } 1931 } ); 1932 1933 // Sort the controls within each section 1934 api.section.each( function ( section ) { 1935 var controls = section.controls(), 1936 controlContainers = _.pluck( controls, 'container' ); 1937 if ( ! section.panel() ) { 1938 rootNodes.push( section ); 1939 } 1940 appendContainer = section.container.find( 'ul:first' ); 1941 if ( ! areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) { 1942 _( controls ).each( function ( control ) { 1943 appendContainer.append( control.container ); 1944 } ); 1945 wasReflowed = true; 1946 } 1947 } ); 1948 1949 // Sort the root panels and sections 1950 rootNodes.sort( function ( a, b ) { 1951 return a.priority() - b.priority(); 1952 } ); 1953 rootContainers = _.pluck( rootNodes, 'container' ); 1954 appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable 1955 if ( ! areElementListsEqual( rootContainers, appendContainer.children() ) ) { 1956 _( rootNodes ).each( function ( rootNode ) { 1957 appendContainer.append( rootNode.container ); 1958 } ); 1959 wasReflowed = true; 1960 } 1961 1962 // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered 1963 api.panel.each( function ( panel ) { 1964 var value = panel.active(); 1965 panel.active.callbacks.fireWith( panel.active, [ value, value ] ); 1966 } ); 1967 api.section.each( function ( section ) { 1968 var value = section.active(); 1969 section.active.callbacks.fireWith( section.active, [ value, value ] ); 1970 } ); 1971 1972 // Restore focus if there was a reflow and there was an active (focused) element 1973 if ( wasReflowed && activeElement ) { 1974 activeElement.focus(); 1975 } 1976 }, api ); 1977 api.bind( 'ready', api.reflowPaneContents ); 1978 api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 ); 1979 $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { 1980 values.bind( 'add', api.reflowPaneContents ); 1981 values.bind( 'change', api.reflowPaneContents ); 1982 values.bind( 'remove', api.reflowPaneContents ); 1983 } ); 1143 1984 1144 1985 // Check if preview url is valid and load the preview frame. … … 1206 2047 }); 1207 2048 2049 // Go back to the top-level Customizer accordion. 2050 $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) { 2051 if ( isKeydownButNotEnterEvent( event ) ) { 2052 return; 2053 } 2054 2055 event.preventDefault(); // Keep this AFTER the key filter above 2056 api.panel.each( function ( panel ) { 2057 panel.collapse(); 2058 }); 2059 }); 2060 1208 2061 closeBtn.keydown( function( event ) { 1209 2062 if ( 9 === event.which ) // tab … … 1220 2073 1221 2074 $('.collapse-sidebar').on( 'click keydown', function( event ) { 1222 if ( event.type === 'keydown' && 13 !== event.which ) // enter2075 if ( isKeydownButNotEnterEvent( event ) ) { 1223 2076 return; 2077 } 1224 2078 1225 2079 overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' ); -
trunk/src/wp-admin/js/customize-widgets.js
r29903 r30102 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 … … 530 547 return; 531 548 } 532 self. toggleForm();549 self.expanded( ! self.expanded() ); 533 550 } ); 534 551 … … 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 } ); … … 778 795 * 779 796 * @param {Boolean} active 780 */ 781 toggle: function ( active ) { 797 * @param {Object} args 798 */ 799 onChangeActive: function ( active, args ) { 800 // Note: there is a second 'args' parameter being passed, merged on top of this.defaultActiveArguments 782 801 this.container.toggleClass( 'widget-rendered', active ); 802 if ( args.completeCallback ) { 803 args.completeCallback(); 804 } 783 805 }, 784 806 … … 1102 1124 */ 1103 1125 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 } 1109 }, 1126 api.Control.prototype.expand.call( this ); 1127 }, 1128 1129 /** 1130 * @param {Boolean} expanded 1131 * @param {Object} [params] 1132 * @returns {Boolean} false if state already applied 1133 */ 1134 _toggleExpanded: api.Section.prototype._toggleExpanded, 1135 1136 /** 1137 * @param {Object} [params] 1138 * @returns {Boolean} false if already expanded 1139 */ 1140 expand: api.Section.prototype.expand, 1110 1141 1111 1142 /** 1112 1143 * Expand the widget form control 1144 * 1145 * @deprecated alias of expand() 1113 1146 */ 1114 1147 expandForm: function() { 1115 this.toggleForm( true ); 1116 }, 1148 this.expand(); 1149 }, 1150 1151 /** 1152 * @param {Object} [params] 1153 * @returns {Boolean} false if already collapsed 1154 */ 1155 collapse: api.Section.prototype.collapse, 1117 1156 1118 1157 /** 1119 1158 * Collapse the widget form control 1159 * 1160 * @deprecated alias of expand() 1120 1161 */ 1121 1162 collapseForm: function() { 1122 this. toggleForm( false);1163 this.collapse(); 1123 1164 }, 1124 1165 … … 1126 1167 * Expand or collapse the widget control 1127 1168 * 1169 * @deprecated this is poor naming, and it is better to directly set control.expanded( showOrHide ) 1170 * 1128 1171 * @param {boolean|undefined} [showOrHide] If not supplied, will be inverse of current visibility 1129 1172 */ 1130 1173 toggleForm: function( showOrHide ) { 1131 var self = this, $widget, $inside, complete; 1174 if ( typeof showOrHide === 'undefined' ) { 1175 showOrHide = ! this.expanded(); 1176 } 1177 this.expanded( showOrHide ); 1178 }, 1179 1180 /** 1181 * Respond to change in the expanded state. 1182 * 1183 * @param {Boolean} expanded 1184 * @param {Object} args merged on top of this.defaultActiveArguments 1185 */ 1186 onChangeExpanded: function ( expanded, args ) { 1187 var self = this, $widget, $inside, complete, prevComplete; 1188 1189 // If the expanded state is unchanged only manipulate container expanded states 1190 if ( args.unchanged ) { 1191 if ( expanded ) { 1192 api.Control.prototype.expand.call( self, { 1193 completeCallback: args.completeCallback 1194 }); 1195 } 1196 return; 1197 } 1132 1198 1133 1199 $widget = this.container.find( 'div.widget:first' ); 1134 1200 $inside = $widget.find( '.widget-inside:first' ); 1135 if ( typeof showOrHide === 'undefined' ) { 1136 showOrHide = ! $inside.is( ':visible' ); 1137 } 1138 1139 // Already expanded or collapsed, so noop 1140 if ( $inside.is( ':visible' ) === showOrHide ) { 1141 return; 1142 } 1143 1144 if ( showOrHide ) { 1201 1202 if ( expanded ) { 1203 1204 self.expandControlSection(); 1205 1145 1206 // Close all other widget controls before expanding this one 1146 1207 api.control.each( function( otherControl ) { 1147 1208 if ( self.params.type === otherControl.params.type && self !== otherControl ) { 1148 otherControl.collapse Form();1209 otherControl.collapse(); 1149 1210 } 1150 1211 } ); … … 1155 1216 self.container.trigger( 'expanded' ); 1156 1217 }; 1218 if ( args.completeCallback ) { 1219 prevComplete = complete; 1220 complete = function () { 1221 prevComplete(); 1222 args.completeCallback(); 1223 }; 1224 } 1157 1225 1158 1226 if ( self.params.is_wide ) { 1159 $inside.fadeIn( 'fast', complete );1227 $inside.fadeIn( args.duration, complete ); 1160 1228 } else { 1161 $inside.slideDown( 'fast', complete );1229 $inside.slideDown( args.duration, complete ); 1162 1230 } 1163 1231 … … 1165 1233 self.container.addClass( 'expanding' ); 1166 1234 } else { 1235 1167 1236 complete = function() { 1168 1237 self.container.removeClass( 'collapsing' ); … … 1170 1239 self.container.trigger( 'collapsed' ); 1171 1240 }; 1241 if ( args.completeCallback ) { 1242 prevComplete = complete; 1243 complete = function () { 1244 prevComplete(); 1245 args.completeCallback(); 1246 }; 1247 } 1172 1248 1173 1249 self.container.trigger( 'collapse' ); … … 1175 1251 1176 1252 if ( self.params.is_wide ) { 1177 $inside.fadeOut( 'fast', complete );1253 $inside.fadeOut( args.duration, complete ); 1178 1254 } else { 1179 $inside.slideUp( 'fast', function() {1255 $inside.slideUp( args.duration, function() { 1180 1256 $widget.css( { width:'', margin:'' } ); 1181 1257 complete(); … … 1183 1259 } 1184 1260 } 1185 },1186 1187 /**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 1261 }, 1196 1262 … … 1305 1371 */ 1306 1372 api.Widgets.SidebarControl = api.Control.extend({ 1373 1307 1374 /** 1308 1375 * Set up the control … … 1326 1393 1327 1394 this.setting.bind( function( newWidgetIds, oldWidgetIds ) { 1328 var widgetFormControls, $sidebarWidgetsAddControl, finalControlContainers, removedWidgetIds;1395 var widgetFormControls, removedWidgetIds, priority; 1329 1396 1330 1397 removedWidgetIds = _( oldWidgetIds ).difference( newWidgetIds ); … … 1351 1418 var aIndex = _.indexOf( newWidgetIds, a.params.widget_id ), 1352 1419 bIndex = _.indexOf( newWidgetIds, b.params.widget_id ); 1353 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 ); 1420 return aIndex - bIndex; 1421 }); 1422 1423 priority = 0; 1424 _( widgetFormControls ).each( function ( control ) { 1425 control.priority( priority ); 1426 control.section( self.section() ); 1427 priority += 1; 1428 }); 1429 self.priority( priority ); // Make sure sidebar control remains at end 1368 1430 1369 1431 // Re-sort widget form controls (including widgets form other sidebars newly moved here) … … 1435 1497 self.active.bind( function ( active ) { 1436 1498 registeredSidebar.set( 'is_rendered', active ); 1437 } ); 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 } 1499 api.section( self.section.get() ).active( active ); 1500 } ); 1501 api.section( self.section.get() ).active( self.active() ); 1467 1502 }, 1468 1503 … … 1501 1536 accept: '.customize-control-widget_form', 1502 1537 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 } 1538 var section = api.section( self.section.get() ); 1539 section.expand({ 1540 allowMultiple: true, // Prevent the section being dragged from to be collapsed 1541 completeCallback: function () { 1542 // @todo It is not clear when refreshPositions should be called on which sections, or if it is even needed 1543 api.section.each( function ( otherSection ) { 1544 if ( otherSection.container.find( '.customize-control-sidebar_widgets' ).length ) { 1545 otherSection.container.find( '.accordion-section-content:first' ).sortable( 'refreshPositions' ); 1546 } 1547 } ); 1548 } 1549 }); 1509 1550 } 1510 1551 }); … … 1549 1590 */ 1550 1591 _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 ); 1555 1556 this.$sectionContent.find( '.customize-control-widget_form:first' ) 1592 var widgetControls = []; 1593 _.each( this.setting(), function ( widgetId ) { 1594 var widgetControl = api.Widgets.getWidgetFormControlForWidget( widgetId ); 1595 if ( widgetControl ) { 1596 widgetControls.push( widgetControl ); 1597 } 1598 }); 1599 1600 if ( ! widgetControls.length ) { 1601 return; 1602 } 1603 1604 $( widgetControls ).each( function () { 1605 $( this.container ) 1606 .removeClass( 'first-widget' ) 1607 .removeClass( 'last-widget' ) 1608 .find( '.move-widget-down, .move-widget-up' ).prop( 'tabIndex', 0 ); 1609 }); 1610 1611 _.first( widgetControls ).container 1557 1612 .addClass( 'first-widget' ) 1558 1613 .find( '.move-widget-up' ).prop( 'tabIndex', -1 ); 1559 1614 1560 this.$sectionContent.find( '.customize-control-widget_form:last' )1615 _.last( widgetControls ).container 1561 1616 .addClass( 'last-widget' ) 1562 1617 .find( '.move-widget-down' ).prop( 'tabIndex', -1 ); … … 1572 1627 * 1573 1628 * @param {Boolean} showOrHide to enable/disable reordering 1629 * 1630 * @todo We should have a reordering state instead and rename this to onChangeReordering 1574 1631 */ 1575 1632 toggleReordering: function( showOrHide ) { … … 1585 1642 if ( showOrHide ) { 1586 1643 _( this.getWidgetFormControls() ).each( function( formControl ) { 1587 formControl.collapse Form();1644 formControl.collapse(); 1588 1645 } ); 1589 1646 … … 1620 1677 */ 1621 1678 addWidget: function( widgetId ) { 1622 var self = this, controlHtml, $widget, controlType = 'widget_form', $control, controlConstructor,1679 var self = this, controlHtml, $widget, controlType = 'widget_form', controlContainer, controlConstructor, 1623 1680 parsedWidgetId = parseWidgetId( widgetId ), 1624 1681 widgetNumber = parsedWidgetId.number, … … 1652 1709 $widget = $( controlHtml ); 1653 1710 1654 $control= $( '<li/>' )1711 controlContainer = $( '<li/>' ) 1655 1712 .addClass( 'customize-control' ) 1656 1713 .addClass( 'customize-control-' + controlType ) … … 1658 1715 1659 1716 // Remove icon which is visible inside the panel 1660 $control.find( '> .widget-icon' ).remove();1717 controlContainer.find( '> .widget-icon' ).remove(); 1661 1718 1662 1719 if ( widget.get( 'is_multi' ) ) { 1663 $control.find( 'input[name="widget_number"]' ).val( widgetNumber );1664 $control.find( 'input[name="multi_number"]' ).val( widgetNumber );1665 } 1666 1667 widgetId = $control.find( '[name="widget-id"]' ).val();1668 1669 $control.hide(); // to be slid-down below1720 controlContainer.find( 'input[name="widget_number"]' ).val( widgetNumber ); 1721 controlContainer.find( 'input[name="multi_number"]' ).val( widgetNumber ); 1722 } 1723 1724 widgetId = controlContainer.find( '[name="widget-id"]' ).val(); 1725 1726 controlContainer.hide(); // to be slid-down below 1670 1727 1671 1728 settingId = 'widget_' + widget.get( 'id_base' ); … … 1673 1730 settingId += '[' + widgetNumber + ']'; 1674 1731 } 1675 $control.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); 1676 1677 this.container.after( $control ); 1732 controlContainer.attr( 'id', 'customize-control-' + settingId.replace( /\]/g, '' ).replace( /\[/g, '-' ) ); 1678 1733 1679 1734 // Only create setting if it doesn't already exist (if we're adding a pre-existing inactive widget) … … 1693 1748 'default': settingId 1694 1749 }, 1750 content: controlContainer, 1695 1751 sidebar_id: self.params.sidebar_id, 1696 1752 widget_id: widgetId, … … 1732 1788 } 1733 1789 1734 $control.slideDown( function() {1790 controlContainer.slideDown( function() { 1735 1791 if ( isExistingWidget ) { 1736 widgetFormControl.expand Form();1792 widgetFormControl.expand(); 1737 1793 widgetFormControl.updateWidget( { 1738 1794 instance: widgetFormControl.setting(), -
trunk/src/wp-includes/class-wp-customize-control.php
r30087 r30102 75 75 76 76 /** 77 * @deprecated It is better to just call the json() method 77 78 * @access public 78 79 * @var array … … 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 … … 242 258 243 259 return true; 260 } 261 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; 244 275 } 245 276 … … 1074 1105 * Widget Area Customize Control Class 1075 1106 * 1107 * @since 3.9.0 1076 1108 */ 1077 1109 class WP_Widget_Area_Customize_Control extends WP_Customize_Control { … … 1115 1147 /** 1116 1148 * Widget Form Customize Control Class 1149 * 1150 * @since 3.9.0 1117 1151 */ 1118 1152 class WP_Widget_Form_Customize_Control extends WP_Customize_Control { -
trunk/src/wp-includes/class-wp-customize-manager.php
r30055 r30102 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 ); … … 511 513 foreach ( $this->settings as $id => $setting ) { 512 514 $settings['values'][ $id ] = $setting->js_value(); 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(); 513 521 } 514 522 foreach ( $this->controls as $id => $control ) { … … 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 } … … 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; -
trunk/src/wp-includes/class-wp-customize-panel.php
r29950 r30102 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 * … … 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 109 134 return $this; 135 } 136 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; 110 189 } 111 190 … … 128 207 129 208 return true; 209 } 210 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; 130 224 } 131 225 … … 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 … … 204 298 </li> 205 299 <?php 206 foreach ( $this->sections as $section ) {207 $section->maybe_render();208 }209 300 } 210 301 } -
trunk/src/wp-includes/class-wp-customize-section.php
r29508 r30102 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 * … … 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 117 143 return $this; 144 } 145 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; 118 198 } 119 199 … … 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; 131 132 if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) ) 211 } 212 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; … … 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 * … … 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 /** … … 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 ); ?>"> … … 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> -
trunk/src/wp-includes/js/customize-base.js
r29907 r30102 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; -
trunk/src/wp-includes/js/customize-preview.js
r29451 r30102 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 } ); -
trunk/tests/qunit/index.html
r27847 r30102 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" /> … … 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> … … 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> … … 39 41 </body> 40 42 </html> 41
Note: See TracChangeset
for help on using the changeset viewer.