From 3c962ee5d80f47b819f06f21a091eb538e72ffef Mon Sep 17 00:00:00 2001 From: Dominik Schilling Date: Wed, 29 Oct 2014 22:51:22 +0000 Subject: [PATCH] Improve/introduce Customizer JavaScript models for Controls, Sections, and Panels. * Introduce models for panels and sections. * Introduce API to expand and focus a control, section or panel. * Allow deep-linking to panels, sections, and controls inside of the Customizer. * Clean up `accordion.js`, removing all Customizer-specific logic. * Add initial unit tests for `wp.customize.Class` in `customize-base.js`. https://make.wordpress.org/core/2014/10/27/toward-a-complete-javascript-api-for-the-customizer/ provides an overview of how to use the JavaScript API. props westonruter, celloexpressions, ryankienstra. see #28032, #28579, #28580, #28650, #28709, #29758. fixes #29529. Built from https://develop.svn.wordpress.org/trunk@30102 git-svn-id: http://core.svn.wordpress.org/trunk@30102 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/customize.php | 45 +- wp-admin/js/accordion.js | 72 +- wp-admin/js/accordion.min.js | 2 +- wp-admin/js/customize-controls.js | 1024 ++++++++++++++++++-- wp-admin/js/customize-controls.min.js | 2 +- wp-admin/js/customize-widgets.js | 260 +++-- wp-admin/js/customize-widgets.min.js | 2 +- wp-includes/class-wp-customize-control.php | 36 +- wp-includes/class-wp-customize-manager.php | 16 +- wp-includes/class-wp-customize-panel.php | 99 +- wp-includes/class-wp-customize-section.php | 117 ++- wp-includes/js/customize-base.js | 3 +- wp-includes/js/customize-preview.js | 2 + wp-includes/js/customize-preview.min.js | 2 +- wp-includes/version.php | 2 +- 15 files changed, 1387 insertions(+), 297 deletions(-) diff --git a/wp-admin/customize.php b/wp-admin/customize.php index 1d795980f7..50ee0ad111 100644 --- a/wp-admin/customize.php +++ b/wp-admin/customize.php @@ -53,8 +53,6 @@ do_action( 'customize_controls_init' ); wp_enqueue_script( 'customize-controls' ); wp_enqueue_style( 'customize-controls' ); -wp_enqueue_script( 'accordion' ); - /** * Enqueue Customizer control scripts. * @@ -130,7 +128,7 @@ do_action( 'customize_controls_print_scripts' ); ?>
-
+
-
    - containers() as $container ) { - $container->maybe_render(); - } - ?> -
+
+
+
@@ -252,10 +246,13 @@ do_action( 'customize_controls_print_scripts' ); ), 'settings' => array(), 'controls' => array(), + 'panels' => array(), + 'sections' => array(), 'nonce' => array( 'save' => wp_create_nonce( 'save-customize_' . $wp_customize->get_stylesheet() ), 'preview' => wp_create_nonce( 'preview-customize_' . $wp_customize->get_stylesheet() ) ), + 'autofocus' => array(), ); // Prepare Customize Setting objects to pass to Javascript. @@ -266,10 +263,32 @@ do_action( 'customize_controls_print_scripts' ); ); } - // Prepare Customize Control objects to pass to Javascript. + // Prepare Customize Control objects to pass to JavaScript. foreach ( $wp_customize->controls() as $id => $control ) { - $control->to_json(); - $settings['controls'][ $id ] = $control->json; + $settings['controls'][ $id ] = $control->json(); + } + + // Prepare Customize Section objects to pass to JavaScript. + foreach ( $wp_customize->sections() as $id => $section ) { + $settings['sections'][ $id ] = $section->json(); + } + + // Prepare Customize Panel objects to pass to JavaScript. + foreach ( $wp_customize->panels() as $id => $panel ) { + $settings['panels'][ $id ] = $panel->json(); + foreach ( $panel->sections as $section_id => $section ) { + $settings['sections'][ $section_id ] = $section->json(); + } + } + + // Pass to frontend the Customizer construct being deeplinked + if ( isset( $_GET['autofocus'] ) && is_array( $_GET['autofocus'] ) ) { + $autofocus = wp_unslash( $_GET['autofocus'] ); + foreach ( $autofocus as $type => $id ) { + if ( isset( $settings[ $type . 's' ][ $id ] ) ) { + $settings['autofocus'][ $type ] = $id; + } + } } ?> diff --git a/wp-admin/js/accordion.js b/wp-admin/js/accordion.js index 6cb1c1ca85..1769d27da9 100644 --- a/wp-admin/js/accordion.js +++ b/wp-admin/js/accordion.js @@ -25,9 +25,6 @@ * * Note that any appropriate tags may be used, as long as the above classes are present. * - * In addition to the standard accordion behavior, this file includes JS for the - * Customizer's "Panel" functionality. - * * @since 3.6.0. */ @@ -46,20 +43,8 @@ accordionSwitch( $( this ) ); }); - // Go back to the top-level Customizer accordion. - $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( e ) { - if ( e.type === 'keydown' && 13 !== e.which ) { // "return" key - return; - } - - e.preventDefault(); // Keep this AFTER the key filter above - - panelSwitch( $( '.current-panel' ) ); - }); }); - var sectionContent = $( '.accordion-section-content' ); - /** * Close the current accordion section and open a new one. * @@ -69,75 +54,22 @@ function accordionSwitch ( el ) { var section = el.closest( '.accordion-section' ), siblings = section.closest( '.accordion-container' ).find( '.open' ), - content = section.find( sectionContent ); + content = section.find( '.accordion-section-content' ); // This section has no content and cannot be expanded. if ( section.hasClass( 'cannot-expand' ) ) { return; } - // Slide into a sub-panel instead of accordioning (Customizer-specific). - if ( section.hasClass( 'control-panel' ) ) { - panelSwitch( section ); - return; - } - if ( section.hasClass( 'open' ) ) { section.toggleClass( 'open' ); content.toggle( true ).slideToggle( 150 ); } else { siblings.removeClass( 'open' ); - siblings.find( sectionContent ).show().slideUp( 150 ); + siblings.find( '.accordion-section-content' ).show().slideUp( 150 ); content.toggle( false ).slideToggle( 150 ); section.toggleClass( 'open' ); } } - /** - * Slide into an accordion sub-panel. - * - * For the Customizer-specific panel functionality - * - * @param {Object} panel Title element or back button of the accordion panel to toggle. - * @since 4.0.0 - */ - function panelSwitch( panel ) { - var position, scroll, - section = panel.closest( '.accordion-section' ), - overlay = section.closest( '.wp-full-overlay' ), - container = section.closest( '.accordion-container' ), - siblings = container.find( '.open' ), - topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), - backBtn = overlay.find( '.control-panel-back' ), - panelTitle = section.find( '.accordion-section-title' ).first(), - content = section.find( '.control-panel-content' ); - - if ( section.hasClass( 'current-panel' ) ) { - section.toggleClass( 'current-panel' ); - overlay.toggleClass( 'in-sub-panel' ); - content.delay( 180 ).hide( 0, function() { - content.css( 'margin-top', 'inherit' ); // Reset - } ); - topPanel.attr( 'tabindex', '0' ); - backBtn.attr( 'tabindex', '-1' ); - panelTitle.focus(); - container.scrollTop( 0 ); - } else { - // Close all open sections in any accordion level. - siblings.removeClass( 'open' ); - siblings.find( sectionContent ).show().slideUp( 0 ); - content.show( 0, function() { - position = content.offset().top; - scroll = container.scrollTop(); - content.css( 'margin-top', ( 45 - position - scroll ) ); - section.toggleClass( 'current-panel' ); - overlay.toggleClass( 'in-sub-panel' ); - container.scrollTop( 0 ); - } ); - topPanel.attr( 'tabindex', '-1' ); - backBtn.attr( 'tabindex', '0' ); - backBtn.focus(); - } - } - })(jQuery); diff --git a/wp-admin/js/accordion.min.js b/wp-admin/js/accordion.min.js index 6b85264342..bcdf3aad96 100644 --- a/wp-admin/js/accordion.min.js +++ b/wp-admin/js/accordion.min.js @@ -1 +1 @@ -!function(a){function b(a){var b=a.closest(".accordion-section"),e=b.closest(".accordion-container").find(".open"),f=b.find(d);if(!b.hasClass("cannot-expand"))return b.hasClass("control-panel")?void c(b):void(b.hasClass("open")?(b.toggleClass("open"),f.toggle(!0).slideToggle(150)):(e.removeClass("open"),e.find(d).show().slideUp(150),f.toggle(!1).slideToggle(150),b.toggleClass("open")))}function c(a){var b,c,e=a.closest(".accordion-section"),f=e.closest(".wp-full-overlay"),g=e.closest(".accordion-container"),h=g.find(".open"),i=f.find("#customize-theme-controls > ul > .accordion-section > .accordion-section-title").add("#customize-info > .accordion-section-title"),j=f.find(".control-panel-back"),k=e.find(".accordion-section-title").first(),l=e.find(".control-panel-content");e.hasClass("current-panel")?(e.toggleClass("current-panel"),f.toggleClass("in-sub-panel"),l.delay(180).hide(0,function(){l.css("margin-top","inherit")}),i.attr("tabindex","0"),j.attr("tabindex","-1"),k.focus(),g.scrollTop(0)):(h.removeClass("open"),h.find(d).show().slideUp(0),l.show(0,function(){b=l.offset().top,c=g.scrollTop(),l.css("margin-top",45-b-c),e.toggleClass("current-panel"),f.toggleClass("in-sub-panel"),g.scrollTop(0)}),i.attr("tabindex","-1"),j.attr("tabindex","0"),j.focus())}a(document).ready(function(){a(".accordion-container").on("click keydown",".accordion-section-title",function(c){("keydown"!==c.type||13===c.which)&&(c.preventDefault(),b(a(this)))}),a("#customize-header-actions").on("click keydown",".control-panel-back",function(b){("keydown"!==b.type||13===b.which)&&(b.preventDefault(),c(a(".current-panel")))})});var d=a(".accordion-section-content")}(jQuery); \ No newline at end of file +!function(a){function b(a){var b=a.closest(".accordion-section"),c=b.closest(".accordion-container").find(".open"),d=b.find(".accordion-section-content");b.hasClass("cannot-expand")||(b.hasClass("open")?(b.toggleClass("open"),d.toggle(!0).slideToggle(150)):(c.removeClass("open"),c.find(".accordion-section-content").show().slideUp(150),d.toggle(!1).slideToggle(150),b.toggleClass("open")))}a(document).ready(function(){a(".accordion-container").on("click keydown",".accordion-section-title",function(c){("keydown"!==c.type||13===c.which)&&(c.preventDefault(),b(a(this)))})})}(jQuery); \ No newline at end of file diff --git a/wp-admin/js/customize-controls.js b/wp-admin/js/customize-controls.js index 363e965745..275424e0d4 100644 --- a/wp-admin/js/customize-controls.js +++ b/wp-admin/js/customize-controls.js @@ -1,6 +1,8 @@ /* globals _wpCustomizeHeader, _wpMediaViewsL10n */ (function( exports, $ ){ - var api = wp.customize; + var bubbleChildValueChanges, Container, focus, isKeydownButNotEnterEvent, areElementListsEqual, api = wp.customize; + + // @todo Move private helper functions to wp.customize.utils so they can be unit tested /** * @constructor @@ -31,71 +33,127 @@ }); /** + * Watch all changes to Value properties, and bubble changes to parent Values instance + * + * @param {wp.customize.Class} instance + * @param {Array} properties The names of the Value instances to watch. + */ + bubbleChildValueChanges = function ( instance, properties ) { + $.each( properties, function ( i, key ) { + instance[ key ].bind( function ( to, from ) { + if ( instance.parent && to !== from ) { + instance.parent.trigger( 'change', instance ); + } + } ); + } ); + }; + + /** + * Expand a panel, section, or control and focus on the first focusable element. + * + * @param {Object} [params] + */ + focus = function ( params ) { + var construct, completeCallback, focus; + construct = this; + params = params || {}; + focus = function () { + construct.container.find( ':focusable:first' ).focus(); + construct.container[0].scrollIntoView( true ); + }; + if ( params.completeCallback ) { + completeCallback = params.completeCallback; + params.completeCallback = function () { + focus(); + completeCallback(); + }; + } else { + params.completeCallback = focus; + } + if ( construct.expand ) { + construct.expand( params ); + } else { + params.completeCallback(); + } + }; + + /** + * Return whether the supplied Event object is for a keydown event but not the Enter key. + * + * @param {jQuery.Event} event + * @returns {boolean} + */ + isKeydownButNotEnterEvent = function ( event ) { + return ( 'keydown' === event.type && 13 !== event.which ); + }; + + /** + * Return whether the two lists of elements are the same and are in the same order. + * + * @param {Array|jQuery} listA + * @param {Array|jQuery} listB + * @returns {boolean} + */ + areElementListsEqual = function ( listA, listB ) { + var equal = ( + listA.length === listB.length && // if lists are different lengths, then naturally they are not equal + -1 === _.map( // are there any false values in the list returned by map? + _.zip( listA, listB ), // pair up each element between the two lists + function ( pair ) { + return $( pair[0] ).is( pair[1] ); // compare to see if each pair are equal + } + ).indexOf( false ) // check for presence of false in map's return value + ); + return equal; + }; + + /** + * Base class for Panel and Section + * * @constructor * @augments wp.customize.Class */ - api.Control = api.Class.extend({ - initialize: function( id, options ) { - var control = this, - nodes, radios, settings; + Container = api.Class.extend({ + defaultActiveArguments: { duration: 'fast' }, + defaultExpandedArguments: { duration: 'fast' }, - this.params = {}; - $.extend( this, options || {} ); + initialize: function ( id, options ) { + var container = this; + container.id = id; + container.params = {}; + $.extend( container, options || {} ); + container.container = $( container.params.content ); - this.id = id; - this.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); - this.container = $( this.selector ); - this.active = new api.Value( this.params.active ); + container.deferred = { + ready: new $.Deferred() + }; + container.priority = new api.Value(); + container.active = new api.Value(); + container.activeArgumentsQueue = []; + container.expanded = new api.Value(); + container.expandedArgumentsQueue = []; - settings = $.map( this.params.settings, function( value ) { - return value; + container.active.bind( function ( active ) { + var args = container.activeArgumentsQueue.shift(); + args = $.extend( {}, container.defaultActiveArguments, args ); + active = ( active && container.isContextuallyActive() ); + container.onChangeActive( active, args ); + // @todo trigger 'activated' and 'deactivated' events based on the expanded param? + }); + container.expanded.bind( function ( expanded ) { + var args = container.expandedArgumentsQueue.shift(); + args = $.extend( {}, container.defaultExpandedArguments, args ); + container.onChangeExpanded( expanded, args ); + // @todo trigger 'expanded' and 'collapsed' events based on the expanded param? }); - api.apply( api, settings.concat( function() { - var key; + container.attachEvents(); - control.settings = {}; - for ( key in control.params.settings ) { - control.settings[ key ] = api( control.params.settings[ key ] ); - } + bubbleChildValueChanges( container, [ 'priority', 'active' ] ); - control.setting = control.settings['default'] || null; - control.renderContent( function() { - // Don't call ready() until the content has rendered. - control.ready(); - } ); - }) ); - - control.elements = []; - - nodes = this.container.find('[data-customize-setting-link]'); - radios = {}; - - nodes.each( function() { - var node = $(this), - name; - - if ( node.is(':radio') ) { - name = node.prop('name'); - if ( radios[ name ] ) - return; - - radios[ name ] = true; - node = nodes.filter( '[name="' + name + '"]' ); - } - - api( node.data('customizeSettingLink'), function( setting ) { - var element = new api.Element( node ); - control.elements.push( element ); - element.sync( setting ); - element.set( setting() ); - }); - }); - - control.active.bind( function ( active ) { - control.toggle( active ); - } ); - control.toggle( control.active() ); + container.priority.set( isNaN( container.params.priority ) ? 100 : container.params.priority ); + container.active.set( container.params.active ); + container.expanded.set( false ); // @todo True if deeplinking? }, /** @@ -104,20 +162,640 @@ ready: function() {}, /** - * Callback for change to the control's active state. + * Get the child models associated with this parent, sorting them by their priority Value. * - * Override function for custom behavior for the control being active/inactive. + * @param {String} parentType + * @param {String} childType + * @returns {Array} + */ + _children: function ( parentType, childType ) { + var parent = this, + children = []; + api[ childType ].each( function ( child ) { + if ( child[ parentType ].get() === parent.id ) { + children.push( child ); + } + } ); + children.sort( function ( a, b ) { + return a.priority() - b.priority(); + } ); + return children; + }, + + /** + * To override by subclass, to return whether the container has active children. + * @abstract + */ + isContextuallyActive: function () { + throw new Error( 'Must override with subclass.' ); + }, + + /** + * Handle changes to the active state. + * This does not change the active state, it merely handles the behavior + * for when it does change. + * + * To override by subclass, update the container's UI to reflect the provided active state. * * @param {Boolean} active + * @param {Object} args merged on top of this.defaultActiveArguments */ - toggle: function ( active ) { + onChangeActive: function ( active, args ) { + var duration = ( 'resolved' === api.previewer.deferred.active.state() ? args.duration : 0 ); if ( active ) { - this.container.slideDown(); + this.container.stop( true, true ).slideDown( duration, args.completeCallback ); } else { - this.container.slideUp(); + this.container.stop( true, true ).slideUp( duration, args.completeCallback ); } }, + /** + * @params {Boolean} active + * @param {Object} [params] + * @returns {Boolean} false if state already applied + */ + _toggleActive: function ( active, params ) { + var self = this; + params = params || {}; + if ( ( active && this.active.get() ) || ( ! active && ! this.active.get() ) ) { + params.unchanged = true; + self.onChangeActive( self.active.get(), params ); + return false; + } else { + params.unchanged = false; + this.activeArgumentsQueue.push( params ); + this.active.set( active ); + return true; + } + }, + + /** + * @param {Object} [params] + * @returns {Boolean} false if already active + */ + activate: function ( params ) { + return this._toggleActive( true, params ); + }, + + /** + * @param {Object} [params] + * @returns {Boolean} false if already inactive + */ + deactivate: function ( params ) { + return this._toggleActive( false, params ); + }, + + /** + * To override by subclass, update the container's UI to reflect the provided active state. + * @abstract + */ + onChangeExpanded: function () { + throw new Error( 'Must override with subclass.' ); + }, + + /** + * @param {Boolean} expanded + * @param {Object} [params] + * @returns {Boolean} false if state already applied + */ + _toggleExpanded: function ( expanded, params ) { + var self = this; + params = params || {}; + if ( ( expanded && this.expanded.get() ) || ( ! expanded && ! this.expanded.get() ) ) { + params.unchanged = true; + self.onChangeExpanded( self.expanded.get(), params ); + return false; + } else { + params.unchanged = false; + this.expandedArgumentsQueue.push( params ); + this.expanded.set( expanded ); + return true; + } + }, + + /** + * @param {Object} [params] + * @returns {Boolean} false if already expanded + */ + expand: function ( params ) { + return this._toggleExpanded( true, params ); + }, + + /** + * @param {Object} [params] + * @returns {Boolean} false if already collapsed + */ + collapse: function ( params ) { + return this._toggleExpanded( false, params ); + }, + + /** + * Bring the container into view and then expand this and bring it into view + * @param {Object} [params] + */ + focus: focus + }); + + /** + * @constructor + * @augments wp.customize.Class + */ + api.Section = Container.extend({ + + /** + * @param {String} id + * @param {Array} options + */ + initialize: function ( id, options ) { + var section = this; + Container.prototype.initialize.call( section, id, options ); + + section.id = id; + section.panel = new api.Value(); + section.panel.bind( function ( id ) { + $( section.container ).toggleClass( 'control-subsection', !! id ); + }); + section.panel.set( section.params.panel || '' ); + bubbleChildValueChanges( section, [ 'panel' ] ); + + section.embed(); + section.deferred.ready.done( function () { + section.ready(); + }); + }, + + /** + * Embed the container in the DOM when any parent panel is ready. + */ + embed: function () { + var section = this, inject; + + // Watch for changes to the panel state + inject = function ( panelId ) { + var parentContainer; + if ( panelId ) { + // The panel has been supplied, so wait until the panel object is registered + api.panel( panelId, function ( panel ) { + // The panel has been registered, wait for it to become ready/initialized + panel.deferred.ready.done( function () { + parentContainer = panel.container.find( 'ul:first' ); + if ( ! section.container.parent().is( parentContainer ) ) { + parentContainer.append( section.container ); + } + section.deferred.ready.resolve(); // @todo Better to use `embedded` instead of `ready` + }); + } ); + } else { + // There is no panel, so embed the section in the root of the customizer + parentContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable + if ( ! section.container.parent().is( parentContainer ) ) { + parentContainer.append( section.container ); + } + section.deferred.ready.resolve(); + } + }; + section.panel.bind( inject ); + inject( section.panel.get() ); // Since a section may never get a panel, assume that it won't ever get one + }, + + /** + * Add behaviors for the accordion section + */ + attachEvents: function () { + var section = this; + + // Expand/Collapse accordion sections on click. + section.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) { + if ( isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); // Keep this AFTER the key filter above + + if ( section.expanded() ) { + section.collapse(); + } else { + section.expand(); + } + }); + }, + + /** + * Return whether this section has any active controls. + * + * @returns {boolean} + */ + isContextuallyActive: function () { + var section = this, + controls = section.controls(), + activeCount = 0; + _( controls ).each( function ( control ) { + if ( control.active() ) { + activeCount += 1; + } + } ); + return ( activeCount !== 0 ); + }, + + /** + * Get the controls that are associated with this section, sorted by their priority Value. + * + * @returns {Array} + */ + controls: function () { + return this._children( 'section', 'control' ); + }, + + /** + * Update UI to reflect expanded state + * + * @param {Boolean} expanded + * @param {Object} args + */ + onChangeExpanded: function ( expanded, args ) { + var section = this, + content = section.container.find( '.accordion-section-content' ), + expand; + + if ( expanded ) { + + if ( args.unchanged ) { + expand = args.completeCallback; + } else { + expand = function () { + content.stop().slideDown( args.duration, args.completeCallback ); + section.container.addClass( 'open' ); + }; + } + + if ( ! args.allowMultiple ) { + api.section.each( function ( otherSection ) { + if ( otherSection !== section ) { + otherSection.collapse( { duration: args.duration } ); + } + }); + } + + if ( section.panel() ) { + api.panel( section.panel() ).expand({ + duration: args.duration, + completeCallback: expand + }); + } else { + expand(); + } + + } else { + section.container.removeClass( 'open' ); + content.slideUp( args.duration, args.completeCallback ); + } + } + }); + + /** + * @constructor + * @augments wp.customize.Class + */ + api.Panel = Container.extend({ + initialize: function ( id, options ) { + var panel = this; + Container.prototype.initialize.call( panel, id, options ); + panel.embed(); + panel.deferred.ready.done( function () { + panel.ready(); + }); + }, + + /** + * Embed the container in the DOM when any parent panel is ready. + */ + embed: function () { + var panel = this, + parentContainer = $( '#customize-theme-controls > ul' ); // @todo This should be defined elsewhere, and to be configurable + + if ( ! panel.container.parent().is( parentContainer ) ) { + parentContainer.append( panel.container ); + } + panel.deferred.ready.resolve(); + }, + + /** + * + */ + attachEvents: function () { + var meta, panel = this; + + // Expand/Collapse accordion sections on click. + panel.container.find( '.accordion-section-title' ).on( 'click keydown', function( event ) { + if ( isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); // Keep this AFTER the key filter above + + if ( ! panel.expanded() ) { + panel.expand(); + } + }); + + meta = panel.container.find( '.panel-meta:first' ); + + meta.find( '> .accordion-section-title' ).on( 'click keydown', function( event ) { + if ( isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); // Keep this AFTER the key filter above + + if ( meta.hasClass( 'cannot-expand' ) ) { + return; + } + + var content = meta.find( '.accordion-section-content:first' ); + if ( meta.hasClass( 'open' ) ) { + meta.toggleClass( 'open' ); + content.slideUp( panel.defaultExpandedArguments.duration ); + } else { + content.slideDown( panel.defaultExpandedArguments.duration ); + meta.toggleClass( 'open' ); + } + }); + + }, + + /** + * Get the sections that are associated with this panel, sorted by their priority Value. + * + * @returns {Array} + */ + sections: function () { + return this._children( 'panel', 'section' ); + }, + + /** + * Return whether this panel has any active sections. + * + * @returns {boolean} + */ + isContextuallyActive: function () { + var panel = this, + sections = panel.sections(), + activeCount = 0; + _( sections ).each( function ( section ) { + if ( section.active() && section.isContextuallyActive() ) { + activeCount += 1; + } + } ); + return ( activeCount !== 0 ); + }, + + /** + * Update UI to reflect expanded state + * + * @param {Boolean} expanded + * @param {Object} args merged with this.defaultExpandedArguments + */ + onChangeExpanded: function ( expanded, args ) { + + // Immediately call the complete callback if there were no changes + if ( args.unchanged ) { + if ( args.completeCallback ) { + args.completeCallback(); + } + return; + } + + // Note: there is a second argument 'args' passed + var position, scroll, + panel = this, + section = panel.container.closest( '.accordion-section' ), + overlay = section.closest( '.wp-full-overlay' ), + container = section.closest( '.accordion-container' ), + siblings = container.find( '.open' ), + topPanel = overlay.find( '#customize-theme-controls > ul > .accordion-section > .accordion-section-title' ).add( '#customize-info > .accordion-section-title' ), + backBtn = overlay.find( '.control-panel-back' ), + panelTitle = section.find( '.accordion-section-title' ).first(), + content = section.find( '.control-panel-content' ); + + if ( expanded ) { + + // Collapse any sibling sections/panels + api.section.each( function ( section ) { + if ( ! section.panel() ) { + section.collapse( { duration: 0 } ); + } + }); + api.panel.each( function ( otherPanel ) { + if ( panel !== otherPanel ) { + otherPanel.collapse( { duration: 0 } ); + } + }); + + content.show( 0, function() { + position = content.offset().top; + scroll = container.scrollTop(); + content.css( 'margin-top', ( 45 - position - scroll ) ); + section.addClass( 'current-panel' ); + overlay.addClass( 'in-sub-panel' ); + container.scrollTop( 0 ); + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + topPanel.attr( 'tabindex', '-1' ); + backBtn.attr( 'tabindex', '0' ); + backBtn.focus(); + } else { + siblings.removeClass( 'open' ); + section.removeClass( 'current-panel' ); + overlay.removeClass( 'in-sub-panel' ); + content.delay( 180 ).hide( 0, function() { + content.css( 'margin-top', 'inherit' ); // Reset + if ( args.completeCallback ) { + args.completeCallback(); + } + } ); + topPanel.attr( 'tabindex', '0' ); + backBtn.attr( 'tabindex', '-1' ); + panelTitle.focus(); + container.scrollTop( 0 ); + } + } + }); + + /** + * @constructor + * @augments wp.customize.Class + */ + api.Control = api.Class.extend({ + defaultActiveArguments: { duration: 'fast' }, + + initialize: function( id, options ) { + var control = this, + nodes, radios, settings; + + control.params = {}; + $.extend( control, options || {} ); + + control.id = id; + control.selector = '#customize-control-' + id.replace( /\]/g, '' ).replace( /\[/g, '-' ); + control.templateSelector = 'customize-control-' + control.params.type + '-content'; + control.container = control.params.content ? $( control.params.content ) : $( control.selector ); + + control.deferred = { + ready: new $.Deferred() + }; + control.section = new api.Value(); + control.priority = new api.Value(); + control.active = new api.Value(); + control.activeArgumentsQueue = []; + + control.elements = []; + + nodes = control.container.find('[data-customize-setting-link]'); + radios = {}; + + nodes.each( function() { + var node = $( this ), + name; + + if ( node.is( ':radio' ) ) { + name = node.prop( 'name' ); + if ( radios[ name ] ) { + return; + } + + radios[ name ] = true; + node = nodes.filter( '[name="' + name + '"]' ); + } + + api( node.data( 'customizeSettingLink' ), function( setting ) { + var element = new api.Element( node ); + control.elements.push( element ); + element.sync( setting ); + element.set( setting() ); + }); + }); + + control.active.bind( function ( active ) { + var args = control.activeArgumentsQueue.shift(); + args = $.extend( {}, control.defaultActiveArguments, args ); + control.onChangeActive( active, args ); + } ); + + control.section.set( control.params.section ); + control.priority.set( isNaN( control.params.priority ) ? 10 : control.params.priority ); + control.active.set( control.params.active ); + + bubbleChildValueChanges( control, [ 'section', 'priority', 'active' ] ); + + // Associate this control with its settings when they are created + settings = $.map( control.params.settings, function( value ) { + return value; + }); + api.apply( api, settings.concat( function () { + var key; + + control.settings = {}; + for ( key in control.params.settings ) { + control.settings[ key ] = api( control.params.settings[ key ] ); + } + + control.setting = control.settings['default'] || null; + + control.embed(); + }) ); + + control.deferred.ready.done( function () { + control.ready(); + }); + }, + + /** + * + */ + embed: function () { + var control = this, + inject; + + // Watch for changes to the section state + inject = function ( sectionId ) { + var parentContainer; + if ( ! sectionId ) { // @todo allow a control to be embeded without a section, for instance a control embedded in the frontend + return; + } + // Wait for the section to be registered + api.section( sectionId, function ( section ) { + // Wait for the section to be ready/initialized + section.deferred.ready.done( function () { + parentContainer = section.container.find( 'ul:first' ); + if ( ! control.container.parent().is( parentContainer ) ) { + parentContainer.append( control.container ); + control.renderContent(); + } + control.deferred.ready.resolve(); // @todo Better to use `embedded` instead of `ready` + }); + }); + }; + control.section.bind( inject ); + inject( control.section.get() ); + }, + + /** + * @abstract + */ + ready: function() {}, + + /** + * Normal controls do not expand, so just expand its parent + * + * @param {Object} [params] + */ + expand: function ( params ) { + api.section( this.section() ).expand( params ); + }, + + /** + * Bring the containing section and panel into view and then this control into view, focusing on the first input + */ + focus: focus, + + /** + * Update UI in response to a change in the control's active state. + * This does not change the active state, it merely handles the behavior + * for when it does change. + * + * @param {Boolean} active + * @param {Object} args merged on top of this.defaultActiveArguments + */ + onChangeActive: function ( active, args ) { + if ( active ) { + this.container.slideDown( args.duration, args.completeCallback ); + } else { + this.container.slideUp( args.duration, args.completeCallback ); + } + }, + + /** + * @deprecated alias of onChangeActive + */ + toggle: function ( active ) { + return this.onChangeActive( active, this.defaultActiveArguments ); + }, + + /** + * Shorthand way to enable the active state. + * + * @param {Object} [params] + * @returns {Boolean} false if already active + */ + activate: Container.prototype.activate, + + /** + * Shorthand way to disable the active state. + * + * @param {Object} [params] + * @returns {Boolean} false if already inactive + */ + deactivate: Container.prototype.deactivate, + dropdownInit: function() { var control = this, statuses = this.container.find('.dropdown-status'), @@ -132,8 +810,9 @@ // Support the .dropdown class to open/close complex elements this.container.on( 'click keydown', '.dropdown', function( event ) { - if ( event.type === 'keydown' && 13 !== event.which ) // enter + if ( isKeydownButNotEnterEvent( event ) ) { return; + } event.preventDefault(); @@ -157,20 +836,18 @@ /** * Render the control from its JS template, if it exists. * - * The control's container must alreasy exist in the DOM. + * The control's container must already exist in the DOM. */ - renderContent: function( callback ) { + renderContent: function () { var template, - selector = 'customize-control-' + this.params.type + '-content'; + control = this; - callback = callback || function(){}; - if ( 0 !== $( '#tmpl-' + selector ).length ) { - template = wp.template( selector ); - if ( template && this.container ) { - this.container.append( template( this.params ) ); + if ( 0 !== $( '#tmpl-' + control.templateSelector ).length ) { + template = wp.template( control.templateSelector ); + if ( template && control.container ) { + control.container.append( template( control.params ) ); } } - callback(); } }); @@ -234,8 +911,9 @@ this.remover = this.container.find('.remove'); this.remover.on( 'click keydown', function( event ) { - if ( event.type === 'keydown' && 13 !== event.which ) // enter + if ( isKeydownButNotEnterEvent( event ) ) { return; + } control.setting.set( control.params.removed ); event.preventDefault(); @@ -306,8 +984,9 @@ // Bind tab switch events this.library.children('ul').on( 'click keydown', 'li', function( event ) { - if ( event.type === 'keydown' && 13 !== event.which ) // enter + if ( isKeydownButNotEnterEvent( event ) ) { return; + } var id = $(this).data('customizeTab'), tab = control.tabs[ id ]; @@ -324,8 +1003,9 @@ // Bind events to switch image urls. this.library.on( 'click keydown', 'a', function( event ) { - if ( event.type === 'keydown' && 13 !== event.which ) // enter + if ( isKeydownButNotEnterEvent( event ) ) { return; + } var value = $(this).data('customizeImageValue'); @@ -597,6 +1277,8 @@ // Create the collection of Control objects. api.control = new api.Values({ defaultConstructor: api.Control }); + api.section = new api.Values({ defaultConstructor: api.Section }); + api.panel = new api.Values({ defaultConstructor: api.Panel }); /** * @constructor @@ -632,29 +1314,42 @@ loaded = false, ready = false; - if ( this._ready ) + if ( this._ready ) { this.unbind( 'ready', this._ready ); + } this._ready = function() { ready = true; - if ( loaded ) + if ( loaded ) { deferred.resolveWith( self ); + } }; this.bind( 'ready', this._ready ); this.bind( 'ready', function ( data ) { - if ( ! data || ! data.activeControls ) { + if ( ! data ) { return; } - $.each( data.activeControls, function ( id, active ) { - var control = api.control( id ); - if ( control ) { - control.active( active ); + var constructs = { + panel: data.activePanels, + section: data.activeSections, + control: data.activeControls + }; + + $.each( constructs, function ( type, activeConstructs ) { + if ( activeConstructs ) { + $.each( activeConstructs, function ( id, active ) { + var construct = api[ type ]( id ); + if ( construct ) { + construct.active( active ); + } + } ); } } ); + } ); this.request = $.ajax( this.previewUrl(), { @@ -676,7 +1371,7 @@ // Check if the location response header differs from the current URL. // If so, the request was redirected; try loading the requested page. - if ( location && location != self.previewUrl() ) { + if ( location && location !== self.previewUrl() ) { deferred.rejectWith( self, [ 'redirect', location ] ); return; } @@ -803,6 +1498,9 @@ rscheme = /^https?/; $.extend( this, options || {} ); + this.deferred = { + active: $.Deferred() + }; /* * Wrap this.refresh to prevent it from hammering the servers: @@ -934,6 +1632,7 @@ self.targetWindow( this.targetWindow() ); self.channel( this.channel() ); + self.deferred.active.resolve(); self.send( 'active' ); }); @@ -1001,6 +1700,8 @@ image: api.ImageControl, header: api.HeaderControl }; + api.panelConstructor = {}; + api.sectionConstructor = {}; $( function() { api.settings = window._wpCustomizeSettings; @@ -1031,6 +1732,29 @@ } }); + // Expand/Collapse the main customizer customize info + $( '#customize-info' ).find( '> .accordion-section-title' ).on( 'click keydown', function( event ) { + if ( isKeydownButNotEnterEvent( event ) ) { + return; + } + event.preventDefault(); // Keep this AFTER the key filter above + + var section = $( this ).parent(), + content = section.find( '.accordion-section-content:first' ); + + if ( section.hasClass( 'cannot-expand' ) ) { + return; + } + + if ( section.hasClass( 'open' ) ) { + section.toggleClass( 'open' ); + content.slideUp( api.Panel.prototype.defaultExpandedArguments.duration ); + } else { + content.slideDown( api.Panel.prototype.defaultExpandedArguments.duration ); + section.toggleClass( 'open' ); + } + }); + // Initialize Previewer api.previewer = new api.Previewer({ container: '#customize-preview', @@ -1124,6 +1848,7 @@ $.extend( this.nonce, nonce ); }); + // Create Settings $.each( api.settings.settings, function( id, data ) { api.create( id, id, data.value, { transport: data.transport, @@ -1131,16 +1856,132 @@ } ); }); + // Create Panels + $.each( api.settings.panels, function ( id, data ) { + var constructor = api.panelConstructor[ data.type ] || api.Panel, + panel; + + panel = new constructor( id, { + params: data + } ); + api.panel.add( id, panel ); + }); + + // Create Sections + $.each( api.settings.sections, function ( id, data ) { + var constructor = api.sectionConstructor[ data.type ] || api.Section, + section; + + section = new constructor( id, { + params: data + } ); + api.section.add( id, section ); + }); + + // Create Controls $.each( api.settings.controls, function( id, data ) { var constructor = api.controlConstructor[ data.type ] || api.Control, control; - control = api.control.add( id, new constructor( id, { + control = new constructor( id, { params: data, previewer: api.previewer - } ) ); + } ); + api.control.add( id, control ); }); + // Focus the autofocused element + _.each( [ 'panel', 'section', 'control' ], function ( type ) { + var instance, id = api.settings.autofocus[ type ]; + if ( id && api[ type ]( id ) ) { + instance = api[ type ]( id ); + // Wait until the element is embedded in the DOM + instance.deferred.ready.done( function () { + // Wait until the preview has activated and so active panels, sections, controls have been set + api.previewer.deferred.active.done( function () { + instance.focus(); + }); + }); + } + }); + + /** + * Sort panels, sections, controls by priorities. Hide empty sections and panels. + */ + api.reflowPaneContents = _.bind( function () { + + var appendContainer, activeElement, rootContainers, rootNodes = [], wasReflowed = false; + + if ( document.activeElement ) { + activeElement = $( document.activeElement ); + } + + // Sort the sections within each panel + api.panel.each( function ( panel ) { + var sections = panel.sections(), + sectionContainers = _.pluck( sections, 'container' ); + rootNodes.push( panel ); + appendContainer = panel.container.find( 'ul:first' ); + if ( ! areElementListsEqual( sectionContainers, appendContainer.children( '[id]' ) ) ) { + _( sections ).each( function ( section ) { + appendContainer.append( section.container ); + } ); + wasReflowed = true; + } + } ); + + // Sort the controls within each section + api.section.each( function ( section ) { + var controls = section.controls(), + controlContainers = _.pluck( controls, 'container' ); + if ( ! section.panel() ) { + rootNodes.push( section ); + } + appendContainer = section.container.find( 'ul:first' ); + if ( ! areElementListsEqual( controlContainers, appendContainer.children( '[id]' ) ) ) { + _( controls ).each( function ( control ) { + appendContainer.append( control.container ); + } ); + wasReflowed = true; + } + } ); + + // Sort the root panels and sections + rootNodes.sort( function ( a, b ) { + return a.priority() - b.priority(); + } ); + rootContainers = _.pluck( rootNodes, 'container' ); + appendContainer = $( '#customize-theme-controls' ).children( 'ul' ); // @todo This should be defined elsewhere, and to be configurable + if ( ! areElementListsEqual( rootContainers, appendContainer.children() ) ) { + _( rootNodes ).each( function ( rootNode ) { + appendContainer.append( rootNode.container ); + } ); + wasReflowed = true; + } + + // Now re-trigger the active Value callbacks to that the panels and sections can decide whether they can be rendered + api.panel.each( function ( panel ) { + var value = panel.active(); + panel.active.callbacks.fireWith( panel.active, [ value, value ] ); + } ); + api.section.each( function ( section ) { + var value = section.active(); + section.active.callbacks.fireWith( section.active, [ value, value ] ); + } ); + + // Restore focus if there was a reflow and there was an active (focused) element + if ( wasReflowed && activeElement ) { + activeElement.focus(); + } + }, api ); + api.bind( 'ready', api.reflowPaneContents ); + api.reflowPaneContents = _.debounce( api.reflowPaneContents, 100 ); + $( [ api.panel, api.section, api.control ] ).each( function ( i, values ) { + values.bind( 'add', api.reflowPaneContents ); + values.bind( 'change', api.reflowPaneContents ); + values.bind( 'remove', api.reflowPaneContents ); + } ); + // Check if preview url is valid and load the preview frame. if ( api.previewer.previewUrl() ) { api.previewer.refresh(); @@ -1205,6 +2046,18 @@ event.preventDefault(); }); + // Go back to the top-level Customizer accordion. + $( '#customize-header-actions' ).on( 'click keydown', '.control-panel-back', function( event ) { + if ( isKeydownButNotEnterEvent( event ) ) { + return; + } + + event.preventDefault(); // Keep this AFTER the key filter above + api.panel.each( function ( panel ) { + panel.collapse(); + }); + }); + closeBtn.keydown( function( event ) { if ( 9 === event.which ) // tab return; @@ -1219,8 +2072,9 @@ }); $('.collapse-sidebar').on( 'click keydown', function( event ) { - if ( event.type === 'keydown' && 13 !== event.which ) // enter + if ( isKeydownButNotEnterEvent( event ) ) { return; + } overlay.toggleClass( 'collapsed' ).toggleClass( 'expanded' ); event.preventDefault(); diff --git a/wp-admin/js/customize-controls.min.js b/wp-admin/js/customize-controls.min.js index 61a2b6d219..5232df0e26 100644 --- a/wp-admin/js/customize-controls.min.js +++ b/wp-admin/js/customize-controls.min.js @@ -1 +1 @@ -!function(a,b){var c=wp.customize;c.Setting=c.Value.extend({initialize:function(a,b,d){c.Value.prototype.initialize.call(this,b,d),this.id=a,this.transport=this.transport||"refresh",this.bind(this.preview)},preview:function(){switch(this.transport){case"refresh":return this.previewer.refresh();case"postMessage":return this.previewer.send("setting",[this.id,this()])}}}),c.Control=c.Class.extend({initialize:function(a,d){var e,f,g,h=this;this.params={},b.extend(this,d||{}),this.id=a,this.selector="#customize-control-"+a.replace(/\]/g,"").replace(/\[/g,"-"),this.container=b(this.selector),this.active=new c.Value(this.params.active),g=b.map(this.params.settings,function(a){return a}),c.apply(c,g.concat(function(){var a;h.settings={};for(a in h.params.settings)h.settings[a]=c(h.params.settings[a]);h.setting=h.settings["default"]||null,h.renderContent(function(){h.ready()})})),h.elements=[],e=this.container.find("[data-customize-setting-link]"),f={},e.each(function(){var a,d=b(this);if(d.is(":radio")){if(a=d.prop("name"),f[a])return;f[a]=!0,d=e.filter('[name="'+a+'"]')}c(d.data("customizeSettingLink"),function(a){var b=new c.Element(d);h.elements.push(b),b.sync(a),b.set(a())})}),h.active.bind(function(a){h.toggle(a)}),h.toggle(h.active())},ready:function(){},toggle:function(a){a?this.container.slideDown():this.container.slideUp()},dropdownInit:function(){var a=this,b=this.container.find(".dropdown-status"),c=this.params,d=!1,e=function(a){"string"==typeof a&&c.statuses&&c.statuses[a]?b.html(c.statuses[a]).show():b.hide()};this.container.on("click keydown",".dropdown",function(b){("keydown"!==b.type||13===b.which)&&(b.preventDefault(),d||a.container.toggleClass("open"),a.container.hasClass("open")&&a.container.parent().parent().find("li.library-selected").focus(),d=!0,setTimeout(function(){d=!1},400))}),this.setting.bind(e),e(this.setting())},renderContent:function(a){var c,d="customize-control-"+this.params.type+"-content";a=a||function(){},0!==b("#tmpl-"+d).length&&(c=wp.template(d),c&&this.container&&this.container.append(c(this.params))),a()}}),c.ColorControl=c.Control.extend({ready:function(){var a=this,b=this.container.find(".color-picker-hex");b.val(a.setting()).wpColorPicker({change:function(){a.setting.set(b.wpColorPicker("color"))},clear:function(){a.setting.set(!1)}})}}),c.UploadControl=c.Control.extend({ready:function(){var a=this;this.params.removed=this.params.removed||"",this.success=b.proxy(this.success,this),this.uploader=b.extend({container:this.container,browser:this.container.find(".upload"),dropzone:this.container.find(".upload-dropzone"),success:this.success,plupload:{},params:{}},this.uploader||{}),a.params.extensions&&(a.uploader.plupload.filters=[{title:c.l10n.allowedFiles,extensions:a.params.extensions}]),a.params.context&&(a.uploader.params["post_data[context]"]=this.params.context),c.settings.theme.stylesheet&&(a.uploader.params["post_data[theme]"]=c.settings.theme.stylesheet),this.uploader=new wp.Uploader(this.uploader),this.remover=this.container.find(".remove"),this.remover.on("click keydown",function(b){("keydown"!==b.type||13===b.which)&&(a.setting.set(a.params.removed),b.preventDefault())}),this.removerVisibility=b.proxy(this.removerVisibility,this),this.setting.bind(this.removerVisibility),this.removerVisibility(this.setting.get())},success:function(a){this.setting.set(a.get("url"))},removerVisibility:function(a){this.remover.toggle(a!=this.params.removed)}}),c.ImageControl=c.UploadControl.extend({ready:function(){var a,d=this;this.uploader={init:function(){var a,b;this.supports.dragdrop||(a=d.container.find(".upload-fallback"),b=a.children().detach(),this.browser.detach().empty().append(b),a.append(this.browser).show())}},c.UploadControl.prototype.ready.call(this),this.thumbnail=this.container.find(".preview-thumbnail img"),this.thumbnailSrc=b.proxy(this.thumbnailSrc,this),this.setting.bind(this.thumbnailSrc),this.library=this.container.find(".library"),this.tabs={},a=this.library.find(".library-content"),this.library.children("ul").children("li").each(function(){var c=b(this),e=c.data("customizeTab"),f=a.filter('[data-customize-tab="'+e+'"]');d.tabs[e]={both:c.add(f),link:c,panel:f}}),this.library.children("ul").on("click keydown","li",function(a){if("keydown"!==a.type||13===a.which){var c=b(this).data("customizeTab"),e=d.tabs[c];a.preventDefault(),e.link.hasClass("library-selected")||(d.selected.both.removeClass("library-selected"),d.selected=e,d.selected.both.addClass("library-selected"))}}),this.library.on("click keydown","a",function(a){if("keydown"!==a.type||13===a.which){var c=b(this).data("customizeImageValue");c&&(d.setting.set(c),a.preventDefault())}}),this.tabs.uploaded&&(this.tabs.uploaded.target=this.library.find(".uploaded-target"),this.tabs.uploaded.panel.find(".thumbnail").length||this.tabs.uploaded.both.addClass("hidden")),a.each(function(){var a=d.tabs[b(this).data("customizeTab")];return a.link.hasClass("hidden")?void 0:(d.selected=a,a.both.addClass("library-selected"),!1)}),this.dropdownInit()},success:function(a){c.UploadControl.prototype.success.call(this,a),this.tabs.uploaded&&this.tabs.uploaded.target.length&&(this.tabs.uploaded.both.removeClass("hidden"),a.element=b('').data("customizeImageValue",a.get("url")).append('').appendTo(this.tabs.uploaded.target))},thumbnailSrc:function(a){/^(https?:)?\/\//.test(a)?this.thumbnail.prop("src",a).show():this.thumbnail.hide()}}),c.HeaderControl=c.Control.extend({ready:function(){this.btnRemove=b("#customize-control-header_image .actions .remove"),this.btnNew=b("#customize-control-header_image .actions .new"),_.bindAll(this,"openMedia","removeImage"),this.btnNew.on("click",this.openMedia),this.btnRemove.on("click",this.removeImage),c.HeaderTool.currentHeader=new c.HeaderTool.ImageModel,new c.HeaderTool.CurrentView({model:c.HeaderTool.currentHeader,el:".current .container"}),new c.HeaderTool.ChoiceListView({collection:c.HeaderTool.UploadsList=new c.HeaderTool.ChoiceList,el:".choices .uploaded .list"}),new c.HeaderTool.ChoiceListView({collection:c.HeaderTool.DefaultsList=new c.HeaderTool.DefaultsList,el:".choices .default .list"}),c.HeaderTool.combinedList=c.HeaderTool.CombinedList=new c.HeaderTool.CombinedList([c.HeaderTool.UploadsList,c.HeaderTool.DefaultsList])},calculateImageSelectOptions:function(a,b){var d,e,f,g,h,i,j=parseInt(_wpCustomizeHeader.data.width,10),k=parseInt(_wpCustomizeHeader.data.height,10),l=!!parseInt(_wpCustomizeHeader.data["flex-width"],10),m=!!parseInt(_wpCustomizeHeader.data["flex-height"],10);return h=a.get("width"),g=a.get("height"),this.headerImage=new c.HeaderTool.ImageModel,this.headerImage.set({themeWidth:j,themeHeight:k,themeFlexWidth:l,themeFlexHeight:m,imageWidth:h,imageHeight:g}),b.set("canSkipCrop",!this.headerImage.shouldBeCropped()),d=j/k,e=h,f=g,e/f>d?(k=f,j=k*d):(j=e,k=j/d),i={handles:!0,keys:!0,instance:!0,persistent:!0,imageWidth:h,imageHeight:g,x1:0,y1:0,x2:j,y2:k},m===!1&&l===!1&&(i.aspectRatio=j+":"+k),m===!1&&(i.maxHeight=k),l===!1&&(i.maxWidth=j),i},openMedia:function(a){var b=_wpMediaViewsL10n;a.preventDefault(),this.frame=wp.media({button:{text:b.selectAndCrop,close:!1},states:[new wp.media.controller.Library({title:b.chooseImage,library:wp.media.query({type:"image"}),multiple:!1,priority:20,suggestedWidth:_wpCustomizeHeader.data.width,suggestedHeight:_wpCustomizeHeader.data.height}),new wp.media.controller.Cropper({imgSelectOptions:this.calculateImageSelectOptions})]}),this.frame.on("select",this.onSelect,this),this.frame.on("cropped",this.onCropped,this),this.frame.on("skippedcrop",this.onSkippedCrop,this),this.frame.open()},onSelect:function(){this.frame.setState("cropper")},onCropped:function(a){var b=a.post_content,c=a.attachment_id,d=a.width,e=a.height;this.setImageFromURL(b,c,d,e)},onSkippedCrop:function(a){var b=a.get("url"),c=a.get("width"),d=a.get("height");this.setImageFromURL(b,a.id,c,d)},setImageFromURL:function(a,b,d,e){var f,g={};g.url=a,g.thumbnail_url=a,g.timestamp=_.now(),b&&(g.attachment_id=b),d&&(g.width=d),e&&(g.height=e),f=new c.HeaderTool.ImageModel({header:g,choice:a.split("/").pop()}),c.HeaderTool.UploadsList.add(f),c.HeaderTool.currentHeader.set(f.toJSON()),f.save(),f.importImage()},removeImage:function(){c.HeaderTool.currentHeader.trigger("hide"),c.HeaderTool.CombinedList.trigger("control:removeImage")}}),c.defaultConstructor=c.Setting,c.control=new c.Values({defaultConstructor:c.Control}),c.PreviewFrame=c.Messenger.extend({sensitivity:2e3,initialize:function(a,d){var e=b.Deferred();e.promise(this),this.container=a.container,this.signature=a.signature,b.extend(a,{channel:c.PreviewFrame.uuid()}),c.Messenger.prototype.initialize.call(this,a,d),this.add("previewUrl",a.previewUrl),this.query=b.extend(a.query||{},{customize_messenger_channel:this.channel()}),this.run(e)},run:function(a){var d=this,e=!1,f=!1;this._ready&&this.unbind("ready",this._ready),this._ready=function(){f=!0,e&&a.resolveWith(d)},this.bind("ready",this._ready),this.bind("ready",function(a){a&&a.activeControls&&b.each(a.activeControls,function(a,b){var d=c.control(a);d&&d.active(b)})}),this.request=b.ajax(this.previewUrl(),{type:"POST",data:this.query,xhrFields:{withCredentials:!0}}),this.request.fail(function(){a.rejectWith(d,["request failure"])}),this.request.done(function(c){var g,h=d.request.getResponseHeader("Location"),i=d.signature;return h&&h!=d.previewUrl()?void a.rejectWith(d,["redirect",h]):"0"===c?void d.login(a):"-1"===c?void a.rejectWith(d,["cheatin"]):(g=c.lastIndexOf(i),-1===g||g")?void a.rejectWith(d,["unsigned"]):(c=c.slice(0,g)+c.slice(g+i.length),d.iframe=b("