diff --git a/src/wp-admin/includes/widgets.php b/src/wp-admin/includes/widgets.php index 356970f076..19fd39023c 100644 --- a/src/wp-admin/includes/widgets.php +++ b/src/wp-admin/includes/widgets.php @@ -181,6 +181,11 @@ function wp_widget_control( $sidebar_args ) { $multi_number = isset($sidebar_args['_multi_num']) ? $sidebar_args['_multi_num'] : ''; $add_new = isset($sidebar_args['_add']) ? $sidebar_args['_add'] : ''; + $before_form = isset( $sidebar_args['before_form'] ) ? $sidebar_args['before_form'] : '
'; + $after_form = isset( $sidebar_args['after_form'] ) ? $sidebar_args['after_form'] : '
'; + $before_widget_content = isset( $sidebar_args['before_widget_content'] ) ? $sidebar_args['before_widget_content'] : '
'; + $after_widget_content = isset( $sidebar_args['after_widget_content'] ) ? $sidebar_args['after_widget_content'] : '
'; + $query_arg = array( 'editwidget' => $widget['id'] ); if ( $add_new ) { $query_arg['addnew'] = 1; @@ -225,14 +230,16 @@ function wp_widget_control( $sidebar_args ) {
-
-
- + + " . __('There are no options for this widget.') . "

\n"; ?> -
+ } else { + echo "\t\t

" . __('There are no options for this widget.') . "

\n"; + } + ?> + @@ -252,7 +259,7 @@ function wp_widget_control( $sidebar_args ) {

- +
diff --git a/src/wp-admin/js/customize-widgets.js b/src/wp-admin/js/customize-widgets.js index 6a640e0efa..8edc02c12f 100644 --- a/src/wp-admin/js/customize-widgets.js +++ b/src/wp-admin/js/customize-widgets.js @@ -417,37 +417,104 @@ /** * @since 4.1.0 */ - initialize: function ( id, options ) { + initialize: function( id, options ) { var control = this; - api.Control.prototype.initialize.call( control, id, options ); - control.expanded = new api.Value(); + + control.widgetControlEmbedded = false; + control.widgetContentEmbedded = false; + control.expanded = new api.Value( false ); control.expandedArgumentsQueue = []; - control.expanded.bind( function ( expanded ) { + control.expanded.bind( function( expanded ) { var args = control.expandedArgumentsQueue.shift(); args = $.extend( {}, control.defaultExpandedArguments, args ); control.onChangeExpanded( expanded, args ); }); - control.expanded.set( false ); + + api.Control.prototype.initialize.call( control, id, options ); }, /** - * Set up the control + * Set up the control. + * + * @since 3.9.0 */ ready: function() { - this._setupModel(); - this._setupWideWidget(); - this._setupControlToggle(); - this._setupWidgetTitle(); - this._setupReorderUI(); - this._setupHighlightEffects(); - this._setupUpdateUI(); - this._setupRemoveUI(); + var control = this; + + /* + * Embed a placeholder once the section is expanded. The full widget + * form content will be embedded once the control itself is expanded, + * and at this point the widget-added event will be triggered. + */ + if ( ! control.section() ) { + control.embedWidgetControl(); + } else { + api.section( control.section(), function( section ) { + var onExpanded = function( isExpanded ) { + if ( isExpanded ) { + control.embedWidgetControl(); + section.expanded.unbind( onExpanded ); + } + }; + if ( section.expanded() ) { + onExpanded( true ); + } else { + section.expanded.bind( onExpanded ); + } + } ); + } + }, + + /** + * Embed the .widget element inside the li container. + * + * @since 4.4.0 + */ + embedWidgetControl: function() { + var control = this, widgetControl; + + if ( control.widgetControlEmbedded ) { + return; + } + control.widgetControlEmbedded = true; + + widgetControl = $( control.params.widget_control ); + control.container.append( widgetControl ); + + control._setupModel(); + control._setupWideWidget(); + control._setupControlToggle(); + + control._setupWidgetTitle(); + control._setupReorderUI(); + control._setupHighlightEffects(); + control._setupUpdateUI(); + control._setupRemoveUI(); + }, + + /** + * Embed the actual widget form inside of .widget-content and finally trigger the widget-added event. + * + * @since 4.4.0 + */ + embedWidgetContent: function() { + var control = this, widgetContent; + + control.embedWidgetControl(); + if ( control.widgetContentEmbedded ) { + return; + } + control.widgetContentEmbedded = true; + + widgetContent = $( control.params.widget_content ); + control.container.find( '.widget-content:first' ).append( widgetContent ); /* * Trigger widget-added event so that plugins can attach any event * listeners and dynamic UI elements. */ - $( document ).trigger( 'widget-added', [ this.container.find( '.widget:first' ) ] ); + $( document ).trigger( 'widget-added', [ control.container.find( '.widget:first' ) ] ); + }, /** @@ -1008,6 +1075,9 @@ var self = this, instanceOverride, completeCallback, $widgetRoot, $widgetContent, updateNumber, params, data, $inputs, processing, jqxhr, isChanged; + // The updateWidget logic requires that the form fields to be fully present. + self.embedWidgetContent(); + args = $.extend( { instance: null, complete: null, @@ -1255,6 +1325,11 @@ onChangeExpanded: function ( expanded, args ) { var self = this, $widget, $inside, complete, prevComplete; + self.embedWidgetControl(); // Make sure the outer form is embedded so that the expanded state can be set in the UI. + if ( expanded ) { + self.embedWidgetContent(); + } + // If the expanded state is unchanged only manipulate container expanded states if ( args.unchanged ) { if ( expanded ) { diff --git a/src/wp-includes/class-wp-customize-control.php b/src/wp-includes/class-wp-customize-control.php index f105ab1860..b6ffa1a84c 100644 --- a/src/wp-includes/class-wp-customize-control.php +++ b/src/wp-includes/class-wp-customize-control.php @@ -1487,20 +1487,21 @@ class WP_Widget_Form_Customize_Control extends WP_Customize_Control { public $height; public $is_wide = false; + /** + * Gather control params for exporting to JavaScript. + * + * @global array $wp_registered_widgets + */ public function to_json() { + global $wp_registered_widgets; + parent::to_json(); $exported_properties = array( 'widget_id', 'widget_id_base', 'sidebar_id', 'width', 'height', 'is_wide' ); foreach ( $exported_properties as $key ) { $this->json[ $key ] = $this->$key; } - } - /** - * - * @global array $wp_registered_widgets - */ - public function render_content() { - global $wp_registered_widgets; + // Get the widget_control and widget_content. require_once ABSPATH . '/wp-admin/includes/widgets.php'; $widget = $wp_registered_widgets[ $this->widget_id ]; @@ -1514,9 +1515,17 @@ class WP_Widget_Form_Customize_Control extends WP_Customize_Control { ); $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) ); - echo $this->manager->widgets->get_widget_control( $args ); + $widget_control_parts = $this->manager->widgets->get_widget_control_parts( $args ); + + $this->json['widget_control'] = $widget_control_parts['control']; + $this->json['widget_content'] = $widget_control_parts['content']; } + /** + * Override render_content to be no-op since content is exported via to_json for deferred embedding. + */ + public function render_content() {} + /** * Whether the current widget is rendered on the page. * diff --git a/src/wp-includes/class-wp-customize-widgets.php b/src/wp-includes/class-wp-customize-widgets.php index 8569228e46..a003393298 100644 --- a/src/wp-includes/class-wp-customize-widgets.php +++ b/src/wp-includes/class-wp-customize-widgets.php @@ -898,21 +898,47 @@ final class WP_Customize_Widgets { * @return string Widget control form HTML markup. */ public function get_widget_control( $args ) { + $args[0]['before_form'] = '
'; + $args[0]['after_form'] = '
'; + $args[0]['before_widget_content'] = '
'; + $args[0]['after_widget_content'] = '
'; ob_start(); - call_user_func_array( 'wp_widget_control', $args ); - $replacements = array( - '
' => '
', - '' => '
', - ); - $control_tpl = ob_get_clean(); - - $control_tpl = str_replace( array_keys( $replacements ), array_values( $replacements ), $control_tpl ); - return $control_tpl; } + /** + * Get the widget control markup parts. + * + * @since 4.4.0 + * @access public + * + * @param array $args Widget control arguments. + * @return array { + * @type string $control Markup for widget control wrapping form. + * @type string $content The contents of the widget form itself. + * } + */ + public function get_widget_control_parts( $args ) { + $args[0]['before_widget_content'] = '
'; + $args[0]['after_widget_content'] = '
'; + $control_markup = $this->get_widget_control( $args ); + + $content_start_pos = strpos( $control_markup, $args[0]['before_widget_content'] ); + $content_end_pos = strrpos( $control_markup, $args[0]['after_widget_content'] ); + + $control = substr( $control_markup, 0, $content_start_pos + strlen( $args[0]['before_widget_content'] ) ); + $control .= substr( $control_markup, $content_end_pos ); + $content = trim( substr( + $control_markup, + $content_start_pos + strlen( $args[0]['before_widget_content'] ), + $content_end_pos - $content_start_pos - strlen( $args[0]['before_widget_content'] ) + ) ); + + return compact( 'control', 'content' ); + } + /** * Add hooks for the Customizer preview. * diff --git a/tests/phpunit/tests/customize/widgets.php b/tests/phpunit/tests/customize/widgets.php index 9f30b9495c..50aab183e8 100644 --- a/tests/phpunit/tests/customize/widgets.php +++ b/tests/phpunit/tests/customize/widgets.php @@ -195,4 +195,74 @@ class Tests_WP_Customize_Widgets extends WP_UnitTestCase { $unsanitized_from_js = $this->manager->widgets->sanitize_widget_instance( $sanitized_for_js ); $this->assertEquals( $unsanitized_from_js, $new_categories_instance ); } + + /** + * Get the widget control args for tests. + * + * @return array + */ + function get_test_widget_control_args() { + global $wp_registered_widgets; + require_once ABSPATH . '/wp-admin/includes/widgets.php'; + $widget_id = 'search-2'; + $widget = $wp_registered_widgets[ $widget_id ]; + $args = array( + 'widget_id' => $widget['id'], + 'widget_name' => $widget['name'], + ); + $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $args, 1 => $widget['params'][0] ) ); + return $args; + } + + /** + * @see WP_Customize_Widgets::get_widget_control() + */ + function test_get_widget_control() { + $this->do_customize_boot_actions(); + $widget_control = $this->manager->widgets->get_widget_control( $this->get_test_widget_control_args() ); + + $this->assertContains( '
', $widget_control ); + $this->assertContains( '
', $widget_control ); + $this->assertContains( 'assertContains( 'do_customize_boot_actions(); + $widget_control_parts = $this->manager->widgets->get_widget_control_parts( $this->get_test_widget_control_args() ); + $this->assertArrayHasKey( 'content', $widget_control_parts ); + $this->assertArrayHasKey( 'control', $widget_control_parts ); + + $this->assertContains( '
', $widget_control_parts['control'] ); + $this->assertContains( '
', $widget_control_parts['control'] ); + $this->assertContains( 'assertNotContains( 'assertContains( 'do_customize_boot_actions(); + $control = $this->manager->get_control( 'widget_search[2]' ); + $params = $control->json(); + + $this->assertEquals( 'widget_form', $params['type'] ); + $this->assertRegExp( '#^]+>\s+$#', $params['content'] ); + $this->assertRegExp( '#^]*class=\'widget\'[^>]*#s', $params['widget_control'] ); + $this->assertContains( '
', $params['widget_control'] ); + $this->assertNotContains( 'assertContains( 'assertEquals( 'search-2', $params['widget_id'] ); + $this->assertEquals( 'search', $params['widget_id_base'] ); + $this->assertArrayHasKey( 'sidebar_id', $params ); + $this->assertArrayHasKey( 'width', $params ); + $this->assertArrayHasKey( 'height', $params ); + $this->assertInternalType( 'bool', $params['is_wide'] ); + + } } diff --git a/tests/phpunit/tests/widgets.php b/tests/phpunit/tests/widgets.php index b7cdb14d32..0086518b77 100644 --- a/tests/phpunit/tests/widgets.php +++ b/tests/phpunit/tests/widgets.php @@ -306,8 +306,63 @@ class Tests_Widgets extends WP_UnitTestCase { ob_start(); $result = dynamic_sidebar( 'Sidebar 1' ); ob_end_clean(); - + $this->assertFalse( $result ); } + /** + * @see wp_widget_control() + */ + function test_wp_widget_control() { + global $wp_registered_widgets; + + wp_widgets_init(); + require_once ABSPATH . '/wp-admin/includes/widgets.php'; + $widget_id = 'search-2'; + $widget = $wp_registered_widgets[ $widget_id ]; + $params = array( + 'widget_id' => $widget['id'], + 'widget_name' => $widget['name'], + ); + $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $params, 1 => $widget['params'][0] ) ); + + ob_start(); + call_user_func_array( 'wp_widget_control', $args ); + $control = ob_get_clean(); + $this->assertNotEmpty( $control ); + + $this->assertContains( '
', $control ); + $this->assertContains( '
', $control ); + $this->assertContains( '
', $control ); + $this->assertContains( '
', $control ); + $this->assertContains( '
', $control ); + $this->assertContains( 'assertContains( 'assertContains( '
', $control ); + $this->assertContains( '
', $control ); + $this->assertContains( 'widget-control-remove', $control ); + $this->assertContains( 'widget-control-close', $control ); + $this->assertContains( '
', $control ); + $this->assertContains( ' '', + 'after_form' => '', + 'before_widget_content' => '', + 'after_widget_content' => '', + ); + $params = array_merge( $params, $param_overrides ); + $args = wp_list_widget_controls_dynamic_sidebar( array( 0 => $params, 1 => $widget['params'][0] ) ); + + ob_start(); + call_user_func_array( 'wp_widget_control', $args ); + $control = ob_get_clean(); + $this->assertNotEmpty( $control ); + $this->assertNotContains( '', $control ); + $this->assertNotContains( '
', $control ); + + foreach ( $param_overrides as $contained ) { + $this->assertContains( $contained, $control ); + } + } } diff --git a/tests/qunit/fixtures/customize-settings.js b/tests/qunit/fixtures/customize-settings.js index af1a2cfa7e..39a49f5a4f 100644 --- a/tests/qunit/fixtures/customize-settings.js +++ b/tests/qunit/fixtures/customize-settings.js @@ -1,9 +1,9 @@ window.wp = window.wp || {}; -window.wp.customize = window.wp.customize || { get: function(){} }; +window.wp.customize = window.wp.customize || { get: function() {} }; var customizerRootElement; customizerRootElement = jQuery( '
    ' ); -customizerRootElement.css( { position: 'absolute', left: -10000, top: -10000 } ); // remove from view +customizerRootElement.css( { position: 'absolute', left: -10000, top: -10000 } ); // Remove from view. jQuery( document.body ).append( customizerRootElement ); window._wpCustomizeSettings = { diff --git a/tests/qunit/fixtures/customize-widgets.js b/tests/qunit/fixtures/customize-widgets.js new file mode 100644 index 0000000000..d3f8bb0326 --- /dev/null +++ b/tests/qunit/fixtures/customize-widgets.js @@ -0,0 +1,138 @@ +window._wpCustomizeWidgetsSettings = { + 'nonce': '12cc9d3284', + 'registeredSidebars': [{ + 'name': 'Widget Area', + 'id': 'sidebar-1', + 'description': 'Add widgets here to appear in your sidebar.', + 'class': '', + 'before_widget': '', + 'before_title': '

    ', + 'after_title': '

    ' + }], + 'registeredWidgets': { + 'search-2': { + 'name': 'Search', + 'id': 'search-2', + 'params': [ + { + 'number': 2 + } + ], + 'classname': 'widget_search', + 'description': 'A search form for your site.' + } + }, + 'availableWidgets': [ + { + 'name': 'Search', + 'id': 'search-2', + 'params': [ + { + 'number': 2 + } + ], + 'classname': 'widget_search', + 'description': 'A search form for your site.', + 'temp_id': 'search-__i__', + 'is_multi': true, + 'multi_number': 3, + 'is_disabled': false, + 'id_base': 'search', + 'transport': 'refresh', + 'width': 250, + 'height': 200, + 'is_wide': false + } + ], + 'l10n': { + 'saveBtnLabel': 'Apply', + 'saveBtnTooltip': 'Save and preview changes before publishing them.', + 'removeBtnLabel': 'Remove', + 'removeBtnTooltip': 'Trash widget by moving it to the inactive widgets sidebar.', + 'error': 'An error has occurred. Please reload the page and try again.', + 'widgetMovedUp': 'Widget moved up', + 'widgetMovedDown': 'Widget moved down' + }, + 'tpl': { + 'widgetReorderNav': '
    Move to another area…Move downMove up
    ', + 'moveWidgetArea': '

    Select an area to move this widget into:

      <% _.each( sidebars, function ( sidebar ){ %>
    • <%- sidebar.name %>
    • <% }); %>
    ' + } +}; + +window._wpCustomizeSettings.panels.widgets = { + 'id': 'widgets', + 'description': 'Widgets are independent sections of content that can be placed into widgetized areas provided by your theme (commonly called sidebars).', + 'priority': 110, + 'type': 'default', + 'title': 'Widgets', + 'content': '', + 'active': true, + 'instanceNumber': 1 +}; + +window._wpCustomizeSettings.sections['sidebar-widgets-sidebar-1'] = { + 'id': 'sidebar-widgets-sidebar-1', + 'description': 'Add widgets here to appear in your sidebar.', + 'priority': 0, + 'panel': 'widgets', + 'type': 'sidebar', + 'title': 'Widget Area', + 'content': '', + 'active': false, + 'instanceNumber': 1, + 'customizeAction': 'Customizing ▸ Widgets', + 'sidebarId': 'sidebar-1' +}; + +window._wpCustomizeSettings.settings['widget_search[2]'] = { + 'value': { + 'encoded_serialized_instance': 'YToxOntzOjU6InRpdGxlIjtzOjY6IkJ1c2NhciI7fQ==', + 'title': 'Buscar', + 'is_widget_customizer_js_value': true, + 'instance_hash_key': '45f0a7f15e50bd3be86b141e2a8b3aaf' + }, + 'transport': 'refresh', + 'dirty': false +}; +window._wpCustomizeSettings.settings['sidebars_widgets[sidebar-1]'] = { + 'value': [ 'search-2' ], + 'transport': 'refresh', + 'dirty': false +}; + +window._wpCustomizeSettings.controls['widget_search[2]'] = { + 'settings': { + 'default': 'widget_search[2]' + }, + 'type': 'widget_form', + 'priority': 0, + 'active': false, + 'section': 'sidebar-widgets-sidebar-1', + 'content': '
  • <\/li>', + 'label': 'Search', + 'description': '', + 'instanceNumber': 2, + 'widget_id': 'search-2', + 'widget_id_base': 'search', + 'sidebar_id': 'sidebar-1', + 'width': 250, + 'height': 200, + 'is_wide': false, + 'widget_control': '
    <\/a> Edit<\/span> Add<\/span> Search<\/span> <\/a> <\/div>

    Search<\/span><\/h4><\/div> <\/div>
    <\/div>
    Delete<\/a> | Close<\/a> <\/div>
    <\/span> <\/div>
    <\/div> <\/div> <\/div>
    A search form for your site. <\/div> <\/div>', + 'widget_content': '

  • Add a Widget <\/span> Reorder<\/span> Done<\/span> <\/span> <\/li>', + 'label': '', + 'description': '', + 'instanceNumber': 1, + 'sidebar_id': 'sidebar-1' +}; diff --git a/tests/qunit/index.html b/tests/qunit/index.html index 71599d458f..aa454a9a35 100644 --- a/tests/qunit/index.html +++ b/tests/qunit/index.html @@ -25,6 +25,7 @@ +
  • TinyMCE tests

    @@ -44,6 +45,7 @@ + @@ -54,6 +56,7 @@ + @@ -267,6 +270,36 @@ + + +