diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php index 9be60c7e74..e3ea406f28 100644 --- a/src/wp-admin/customize.php +++ b/src/wp-admin/customize.php @@ -175,7 +175,9 @@ do_action( 'customize_controls_print_scripts' );
render_panel_templates(); + $wp_customize->render_section_templates(); $wp_customize->render_control_templates(); /** @@ -259,28 +261,38 @@ do_action( 'customize_controls_print_scripts' ); // Prepare Customize Setting objects to pass to JavaScript. foreach ( $wp_customize->settings() as $id => $setting ) { - $settings['settings'][ $id ] = array( - 'value' => $setting->js_value(), - 'transport' => $setting->transport, - 'dirty' => $setting->dirty, - ); + if ( $setting->check_capabilities() ) { + $settings['settings'][ $id ] = array( + 'value' => $setting->js_value(), + 'transport' => $setting->transport, + 'dirty' => $setting->dirty, + ); + } } // Prepare Customize Control objects to pass to JavaScript. foreach ( $wp_customize->controls() as $id => $control ) { - $settings['controls'][ $id ] = $control->json(); + if ( $control->check_capabilities() ) { + $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(); + if ( $section->check_capabilities() ) { + $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(); + foreach ( $wp_customize->panels() as $panel_id => $panel ) { + if ( $panel->check_capabilities() ) { + $settings['panels'][ $panel_id ] = $panel->json(); + foreach ( $panel->sections as $section_id => $section ) { + if ( $section->check_capabilities() ) { + $settings['sections'][ $section_id ] = $section->json(); + } + } } } diff --git a/src/wp-admin/js/customize-controls.js b/src/wp-admin/js/customize-controls.js index 7ed309572a..2d7f3bad4a 100644 --- a/src/wp-admin/js/customize-controls.js +++ b/src/wp-admin/js/customize-controls.js @@ -156,6 +156,7 @@ Container = api.Class.extend({ defaultActiveArguments: { duration: 'fast', completeCallback: $.noop }, defaultExpandedArguments: { duration: 'fast', completeCallback: $.noop }, + containerType: 'container', /** * @since 4.1.0 @@ -168,7 +169,11 @@ container.id = id; container.params = {}; $.extend( container, options || {} ); + container.templateSelector = 'customize-' + container.containerType + '-' + container.params.type; container.container = $( container.params.content ); + if ( 0 === container.container.length ) { + container.container = $( container.getContainer() ); + } container.deferred = { embedded: new $.Deferred() @@ -191,7 +196,9 @@ container.onChangeExpanded( expanded, args ); }); - container.attachEvents(); + container.deferred.embedded.done( function () { + container.attachEvents(); + }); api.utils.bubbleChildValueChanges( container, [ 'priority', 'active' ] ); @@ -366,7 +373,26 @@ * Bring the container into view and then expand this and bring it into view * @param {Object} [params] */ - focus: focus + focus: focus, + + /** + * Return the container html, generated from its JS template, if it exists. + * + * @since 4.3.0 + */ + getContainer: function () { + var template, + container = this; + + if ( 0 !== $( '#tmpl-' + container.templateSelector ).length ) { + template = wp.template( container.templateSelector ); + if ( template && container.container ) { + return $.trim( template( container.params ) ); + } + } + + return '
  • '; + } }); /** @@ -376,6 +402,7 @@ * @augments wp.customize.Class */ api.Section = Container.extend({ + containerType: 'section', /** * @since 4.1.0 @@ -977,6 +1004,8 @@ * @augments wp.customize.Class */ api.Panel = Container.extend({ + containerType: 'panel', + /** * @since 4.1.0 * @@ -1003,6 +1032,7 @@ if ( ! panel.container.parent().is( parentContainer ) ) { parentContainer.append( panel.container ); + panel.renderContent(); } panel.deferred.embedded.resolve(); }, @@ -1045,6 +1075,7 @@ } event.preventDefault(); // Keep this AFTER the key filter above + meta = panel.container.find( '.panel-meta' ); if ( meta.hasClass( 'cannot-expand' ) ) { return; } @@ -1169,6 +1200,26 @@ panelTitle.focus(); container.scrollTop( 0 ); } + }, + + /** + * Render the panel from its JS template, if it exists. + * + * The panel's container must already exist in the DOM. + * + * @since 4.3.0 + */ + renderContent: function () { + var template, + panel = this; + + // Add the content to the container. + if ( 0 !== $( '#tmpl-' + panel.templateSelector + '-content' ).length ) { + template = wp.template( panel.templateSelector + '-content' ); + if ( template && panel.container ) { + panel.container.find( '.accordion-sub-container' ).html( template( panel.params ) ); + } + } } }); diff --git a/src/wp-includes/class-wp-customize-manager.php b/src/wp-includes/class-wp-customize-manager.php index 98539b0294..47c0407f56 100644 --- a/src/wp-includes/class-wp-customize-manager.php +++ b/src/wp-includes/class-wp-customize-manager.php @@ -60,7 +60,25 @@ final class WP_Customize_Manager { protected $customized; /** - * Controls that may be rendered from JS templates. + * Panel types that may be rendered from JS templates. + * + * @since 4.3.0 + * @access protected + * @var array + */ + protected $registered_panel_types = array(); + + /** + * Section types that may be rendered from JS templates. + * + * @since 4.3.0 + * @access protected + * @var array + */ + protected $registered_section_types = array(); + + /** + * Control types that may be rendered from JS templates. * * @since 4.1.0 * @access protected @@ -612,19 +630,29 @@ final class WP_Customize_Manager { } foreach ( $this->settings as $id => $setting ) { - $settings['values'][ $id ] = $setting->js_value(); + if ( $setting->check_capabilities() ) { + $settings['values'][ $id ] = $setting->js_value(); + } } - foreach ( $this->panels as $id => $panel ) { - $settings['activePanels'][ $id ] = $panel->active(); - foreach ( $panel->sections as $id => $section ) { - $settings['activeSections'][ $id ] = $section->active(); + foreach ( $this->panels as $panel_id => $panel ) { + if ( $panel->check_capabilities() ) { + $settings['activePanels'][ $panel_id ] = $panel->active(); + foreach ( $panel->sections as $section_id => $section ) { + if ( $section->check_capabilities() ) { + $settings['activeSections'][ $section_id ] = $section->active(); + } + } } } foreach ( $this->sections as $id => $section ) { - $settings['activeSections'][ $id ] = $section->active(); + if ( $section->check_capabilities() ) { + $settings['activeSections'][ $id ] = $section->active(); + } } foreach ( $this->controls as $id => $control ) { - $settings['activeControls'][ $id ] = $control->active(); + if ( $control->check_capabilities() ) { + $settings['activeControls'][ $id ] = $control->active(); + } } ?> @@ -964,6 +992,34 @@ final class WP_Customize_Manager { unset( $this->panels[ $id ] ); } + /** + * Register a customize panel type. + * + * Registered types are eligible to be rendered via JS and created dynamically. + * + * @since 4.3.0 + * @access public + * + * @param string $panel Name of a custom panel which is a subclass of + * {@see WP_Customize_Panel}. + */ + public function register_panel_type( $panel ) { + $this->registered_panel_types[] = $panel; + } + + /** + * Render JS templates for all registered panel types. + * + * @since 4.3.0 + * @access public + */ + public function render_panel_templates() { + foreach ( $this->registered_panel_types as $panel_type ) { + $panel = new $panel_type( $this, 'temp', array() ); + $panel->print_template(); + } + } + /** * Add a customize section. * @@ -1005,6 +1061,34 @@ final class WP_Customize_Manager { unset( $this->sections[ $id ] ); } + /** + * Register a customize section type. + * + * Registered types are eligible to be rendered via JS and created dynamically. + * + * @since 4.3.0 + * @access public + * + * @param string $section Name of a custom section which is a subclass of + * {@see WP_Customize_Section}. + */ + public function register_section_type( $section ) { + $this->registered_section_types[] = $section; + } + + /** + * Render JS templates for all registered section types. + * + * @since 4.3.0 + * @access public + */ + public function render_section_templates() { + foreach ( $this->registered_section_types as $section_type ) { + $section = new $section_type( $this, 'temp', array() ); + $section->print_template(); + } + } + /** * Add a customize control. * @@ -1176,7 +1260,10 @@ final class WP_Customize_Manager { */ public function register_controls() { - /* Control Types (custom control classes) */ + /* Panel, Section, and Control Types */ + $this->register_panel_type( 'WP_Customize_Panel' ); + $this->register_section_type( 'WP_Customize_Section' ); + $this->register_section_type( 'WP_Customize_Sidebar_Section' ); $this->register_control_type( 'WP_Customize_Color_Control' ); $this->register_control_type( 'WP_Customize_Media_Control' ); $this->register_control_type( 'WP_Customize_Upload_Control' ); diff --git a/src/wp-includes/class-wp-customize-panel.php b/src/wp-includes/class-wp-customize-panel.php index f977f06867..14a47b4e17 100644 --- a/src/wp-includes/class-wp-customize-panel.php +++ b/src/wp-includes/class-wp-customize-panel.php @@ -214,7 +214,7 @@ class WP_Customize_Panel { * @return array The array to be exported to the client as JSON. */ public function json() { - $array = wp_array_slice_assoc( (array) $this, array( 'title', 'description', 'priority', 'type' ) ); + $array = wp_array_slice_assoc( (array) $this, array( 'id', 'title', 'description', 'priority', 'type' ) ); $array['content'] = $this->get_content(); $array['active'] = $this->active(); $array['instanceNumber'] = $this->instance_number; @@ -289,48 +289,92 @@ class WP_Customize_Panel { } /** - * Render the panel container, and then its contents. + * Render the panel container, and then its contents (via `this->render_content()`) in a subclass. + * + * Panel containers are now rendered in JS by default, see {@see WP_Customize_Panel::print_template()}. * * @since 4.0.0 * @access protected */ - protected function render() { - $classes = 'accordion-section control-section control-panel control-panel-' . $this->type; + protected function render() {} + + /** + * Render the panel UI in a subclass. + * + * Panel contents are now rendered in JS by default, see {@see WP_Customize_Panel::print_template()}. + * + * @since 4.1.0 + * @access protected + */ + protected function render_content() {} + + /** + * Render the panel's JS templates. + * + * This function is only run for panel types that have been registered with + * {@see WP_Customize_Manager::register_panel_type()}. + * + * @since 4.3.0 + */ + public function print_template() { ?> -
  • + + + +
  • - title ); ?> + {{ data.title }}

    - +
  • -
  • +
  • ' . esc_html( $this->title ) . '' ); + echo sprintf( __( 'You are customizing %s' ), '{{ data.title }}' ); ?>
    - description ) ) : ?> + <# if ( data.description ) { #>
    - description; ?> + {{{ data.description }}}
    - + <# } #>
  • get_content(); $array['active'] = $this->active(); $array['instanceNumber'] = $this->instance_number; + + if ( $this->panel ) { + /* translators: ▸ is the unicode right-pointing triangle, and %s is the section title in the Customizer */ + $array['customizeAction'] = sprintf( __( 'Customizing ▸ %s' ), esc_html( $this->manager->get_panel( $this->panel )->title ) ); + } else { + $array['customizeAction'] = __( 'Customizing' ); + } + return $array; } @@ -251,7 +259,7 @@ class WP_Customize_Section { } /** - * Get the section's content template for insertion into the Customizer pane. + * Get the section's content for insertion into the Customizer pane. * * @since 4.1.0 * @@ -297,16 +305,45 @@ class WP_Customize_Section { } /** - * Render the section, and the controls that have been added to it. + * Render the section UI in a subclass. + * + * Sections are now rendered in JS by default, see {@see WP_Customize_Section::print_template()}. * * @since 3.4.0 */ - protected function render() { - $classes = 'accordion-section control-section control-section-' . $this->type; + protected function render() {} + + /** + * Render the section's JS template. + * + * This function is only run for section types that have been registered with + * {@see WP_Customize_Manager::register_section_type()}. + * + * @since 4.3.0 + */ + public function print_template() { + ?> + + -
  • +
  • - title ); ?> + {{ data.title }}

  • diff --git a/tests/phpunit/tests/customize/panel.php b/tests/phpunit/tests/customize/panel.php new file mode 100644 index 0000000000..d1c3bb1768 --- /dev/null +++ b/tests/phpunit/tests/customize/panel.php @@ -0,0 +1,238 @@ +manager = $GLOBALS['wp_customize']; + $this->undefined = new stdClass(); + } + + function tearDown() { + $this->manager = null; + unset( $GLOBALS['wp_customize'] ); + parent::tearDown(); + } + + /** + * @see WP_Customize_Panel::__construct() + */ + function test_construct_default_args() { + $panel = new WP_Customize_Panel( $this->manager, 'foo' ); + $this->assertInternalType( 'int', $panel->instance_number ); + $this->assertEquals( $this->manager, $panel->manager ); + $this->assertEquals( 'foo', $panel->id ); + $this->assertEquals( 160, $panel->priority ); + $this->assertEquals( 'edit_theme_options', $panel->capability ); + $this->assertEquals( '', $panel->theme_supports ); + $this->assertEquals( '', $panel->title ); + $this->assertEquals( '', $panel->description ); + $this->assertEmpty( $panel->sections ); + $this->assertEquals( 'default', $panel->type ); + $this->assertEquals( array( $panel, 'active_callback' ), $panel->active_callback ); + } + + /** + * @see WP_Customize_Panel::__construct() + */ + function test_construct_custom_args() { + $args = array( + 'priority' => 200, + 'capability' => 'edit_posts', + 'theme_supports' => 'html5', + 'title' => 'Hello World', + 'description' => 'Lorem Ipsum', + 'type' => 'horizontal', + 'active_callback' => '__return_true', + ); + + $panel = new WP_Customize_Panel( $this->manager, 'foo', $args ); + foreach ( $args as $key => $value ) { + $this->assertEquals( $value, $panel->$key ); + } + } + + /** + * @see WP_Customize_Panel::__construct() + */ + function test_construct_custom_type() { + $panel = new Custom_Panel_Test( $this->manager, 'foo' ); + $this->assertEquals( 'titleless', $panel->type ); + } + + /** + * @see WP_Customize_Panel::active() + * @see WP_Customize_Panel::active_callback() + */ + function test_active() { + $panel = new WP_Customize_Panel( $this->manager, 'foo' ); + $this->assertTrue( $panel->active() ); + + $panel = new WP_Customize_Panel( $this->manager, 'foo', array( + 'active_callback' => '__return_false', + ) ); + $this->assertFalse( $panel->active() ); + add_filter( 'customize_panel_active', array( $this, 'filter_active_test' ), 10, 2 ); + $this->assertTrue( $panel->active() ); + } + + /** + * @param bool $active + * @param WP_Customize_Panel $panel + * @return bool + */ + function filter_active_test( $active, $panel ) { + $this->assertFalse( $active ); + $this->assertInstanceOf( 'WP_Customize_Panel', $panel ); + $active = true; + return $active; + } + + /** + * @see WP_Customize_Panel::json() + */ + function test_json() { + $args = array( + 'priority' => 200, + 'capability' => 'edit_posts', + 'theme_supports' => 'html5', + 'title' => 'Hello World', + 'description' => 'Lorem Ipsum', + 'type' => 'horizontal', + 'active_callback' => '__return_true', + ); + $panel = new WP_Customize_Panel( $this->manager, 'foo', $args ); + $data = $panel->json(); + $this->assertEquals( 'foo', $data['id'] ); + foreach ( array( 'title', 'description', 'priority', 'type' ) as $key ) { + $this->assertEquals( $args[ $key ], $data[ $key ] ); + } + $this->assertEmpty( $data['content'] ); + $this->assertTrue( $data['active'] ); + $this->assertInternalType( 'int', $data['instanceNumber'] ); + } + + /** + * @see WP_Customize_Panel::check_capabilities() + */ + function test_check_capabilities() { + $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + + $panel = new WP_Customize_Panel( $this->manager, 'foo' ); + $this->assertTrue( $panel->check_capabilities() ); + $old_cap = $panel->capability; + $panel->capability = 'do_not_allow'; + $this->assertFalse( $panel->check_capabilities() ); + $panel->capability = $old_cap; + $this->assertTrue( $panel->check_capabilities() ); + $panel->theme_supports = 'impossible_feature'; + $this->assertFalse( $panel->check_capabilities() ); + } + + /** + * @see WP_Customize_Panel::get_content() + */ + function test_get_content() { + $panel = new WP_Customize_Panel( $this->manager, 'foo' ); + $this->assertEmpty( $panel->get_content() ); + } + + /** + * @see WP_Customize_Panel::maybe_render() + */ + function test_maybe_render() { + wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); + $panel = new WP_Customize_Panel( $this->manager, 'bar' ); + $customize_render_panel_count = did_action( 'customize_render_panel' ); + add_action( 'customize_render_panel', array( $this, 'action_customize_render_panel_test' ) ); + ob_start(); + $panel->maybe_render(); + $content = ob_get_clean(); + $this->assertTrue( $panel->check_capabilities() ); + $this->assertEmpty( $content ); + $this->assertEquals( $customize_render_panel_count + 1, did_action( 'customize_render_panel' ), 'Unexpected did_action count for customize_render_panel' ); + $this->assertEquals( 1, did_action( "customize_render_panel_{$panel->id}" ), "Unexpected did_action count for customize_render_panel_{$panel->id}" ); + } + + /** + * @see WP_Customize_Panel::maybe_render() + * @param WP_Customize_Panel $panel + */ + function action_customize_render_panel_test( $panel ) { + $this->assertInstanceOf( 'WP_Customize_Panel', $panel ); + } + + /** + * @see WP_Customize_Panel::print_template() + */ + function test_print_templates_standard() { + wp_set_current_user( $this->factory->user->create( array( 'role' => 'administrator' ) ) ); + + $panel = new WP_Customize_Panel( $this->manager, 'baz' ); + ob_start(); + $panel->print_template(); + $content = ob_get_clean(); + $this->assertContains( ' + @@ -39,5 +40,73 @@ + + + + + + + + + + diff --git a/tests/qunit/wp-admin/js/customize-controls.js b/tests/qunit/wp-admin/js/customize-controls.js index cb8767fbb4..215d46802f 100644 --- a/tests/qunit/wp-admin/js/customize-controls.js +++ b/tests/qunit/wp-admin/js/customize-controls.js @@ -95,28 +95,61 @@ jQuery( window ).load( function (){ equal( control.section(), 'fixture-section' ); } ); + // Begin sections. module( 'Customizer Section in Fixture' ); test( 'Fixture section exists', function () { ok( wp.customize.section.has( 'fixture-section' ) ); } ); test( 'Fixture section has control among controls()', function () { var section = wp.customize.section( 'fixture-section' ); - equal( section.controls().length, 1 ); - equal( section.controls()[0].id, 'fixture-control' ); + ok( -1 !== _.pluck( section.controls(), 'id' ).indexOf( 'fixture-control' ) ); } ); - test( 'Fixture section has control among controls()', function () { + test( 'Fixture section has has expected panel', function () { var section = wp.customize.section( 'fixture-section' ); equal( section.panel(), 'fixture-panel' ); } ); + module( 'Customizer Default Section with Template in Fixture' ); + test( 'Fixture section exists', function () { + ok( wp.customize.section.has( 'fixture-section-default-templated' ) ); + } ); + test( 'Fixture section has expected content', function () { + var id = 'fixture-section-default-templated', section; + section = wp.customize.section( id ); + ok( ! section.params.content ); + ok( !! section.container ); + ok( section.container.is( '.control-section.control-section-default' ) ); + ok( 1 === section.container.find( '> .accordion-section-title' ).length ); + ok( 1 === section.container.find( '> .accordion-section-content' ).length ); + } ); + + module( 'Customizer Custom Type (titleless) Section with Template in Fixture' ); + test( 'Fixture section exists', function () { + ok( wp.customize.section.has( 'fixture-section-titleless-templated' ) ); + } ); + test( 'Fixture section has expected content', function () { + var id = 'fixture-section-titleless-templated', section; + section = wp.customize.section( id ); + ok( ! section.params.content ); + ok( !! section.container ); + ok( section.container.is( '.control-section.control-section-titleless' ) ); + ok( 0 === section.container.find( '> .accordion-section-title' ).length ); + ok( 1 === section.container.find( '> .accordion-section-content' ).length ); + } ); + + // Begin panels. module( 'Customizer Panel in Fixture' ); test( 'Fixture panel exists', function () { ok( wp.customize.panel.has( 'fixture-panel' ) ); } ); - test( 'Fixture section has control among controls()', function () { + test( 'Fixture panel has content', function () { var panel = wp.customize.panel( 'fixture-panel' ); - equal( panel.sections().length, 1 ); - equal( panel.sections()[0].id, 'fixture-section' ); + ok( !! panel.params.content ); + ok( !! panel.container ); + } ); + test( 'Fixture panel has section among its sections()', function () { + var panel = wp.customize.panel( 'fixture-panel' ); + ok( -1 !== _.pluck( panel.sections(), 'id' ).indexOf( 'fixture-section' ) ); } ); test( 'Panel is not expanded by default', function () { var panel = wp.customize.panel( 'fixture-panel' ); @@ -138,6 +171,34 @@ jQuery( window ).load( function (){ ok( panel.expanded() ); } ); + module( 'Customizer Default Panel with Template in Fixture' ); + test( 'Fixture section exists', function () { + ok( wp.customize.panel.has( 'fixture-panel-default-templated' ) ); + } ); + test( 'Fixture panel has expected content', function () { + var id = 'fixture-panel-default-templated', panel; + panel = wp.customize.panel( id ); + ok( ! panel.params.content ); + ok( !! panel.container ); + ok( panel.container.is( '.control-panel.control-panel-default' ) ); + ok( 1 === panel.container.find( '> .accordion-section-title' ).length ); + ok( 1 === panel.container.find( '> .control-panel-content' ).length ); + } ); + + module( 'Customizer Custom Type Panel (titleless) with Template in Fixture' ); + test( 'Fixture panel exists', function () { + ok( wp.customize.panel.has( 'fixture-panel-titleless-templated' ) ); + } ); + test( 'Fixture panel has expected content', function () { + var id = 'fixture-panel-titleless-templated', panel; + panel = wp.customize.panel( id ); + ok( ! panel.params.content ); + ok( !! panel.container ); + ok( panel.container.is( '.control-panel.control-panel-titleless' ) ); + ok( 0 === panel.container.find( '> .accordion-section-title' ).length ); + ok( 1 === panel.container.find( '> .control-panel-content' ).length ); + } ); + module( 'Dynamically-created Customizer Setting Model' ); settingId = 'new_blogname'; @@ -160,10 +221,11 @@ jQuery( window ).load( function (){ module( 'Dynamically-created Customizer Section Model' ); sectionId = 'mock_title_tagline'; - sectionContent = '
  • '; + sectionContent = '
  • Section Fixture Press return or enter to open

  • '; sectionData = { content: sectionContent, - active: true + active: true, + type: 'default' }; mockSection = new wp.customize.Section( sectionId, { params: sectionData } ); @@ -277,7 +339,8 @@ jQuery( window ).load( function (){ content: panelContent, title: panelTitle, description: panelDescription, - active: true // @todo This should default to true + active: true, // @todo This should default to true + type: 'default' }; mockPanel = new wp.customize.Panel( panelId, { params: panelData } );