diff --git a/lib/class-experimental-wp-widget-blocks-manager.php b/lib/class-experimental-wp-widget-blocks-manager.php index c3b609db6738c7..c915807bd769bb 100644 --- a/lib/class-experimental-wp-widget-blocks-manager.php +++ b/lib/class-experimental-wp-widget-blocks-manager.php @@ -121,16 +121,26 @@ public static function get_sidebar_as_blocks( $sidebar_id ) { $wp_registered_sidebars = self::get_wp_registered_sidebars(); foreach ( $sidebars_items[ $sidebar_id ] as $item ) { - $widget_class = self::get_widget_class( $item ); - $blocks[] = array( + $widget_class = self::get_widget_class( $item ); + list( $object, $number ) = self::get_widget_info( $item ); + $new_block = array( 'blockName' => 'core/legacy-widget', 'attrs' => array( - 'class' => $widget_class, - 'identifier' => $item, - 'instance' => self::get_sidebar_widget_instance( $wp_registered_sidebars[ $sidebar_id ], $item ), + 'id' => $item, + 'instance' => self::get_sidebar_widget_instance( $wp_registered_sidebars[ $sidebar_id ], $item ), ), 'innerHTML' => '', ); + if ( null !== $widget_class ) { + $new_block['attrs']['widgetClass'] = $widget_class; + } + if ( isset( $object->id_base ) ) { + $new_block['attrs']['idBase'] = $object->id_base; + } + if ( is_int( $number ) ) { + $new_block['attrs']['number'] = $number; + } + $blocks[] = $new_block; } return $blocks; } diff --git a/lib/class-wp-rest-widget-updater-controller.php b/lib/class-wp-rest-widget-forms.php similarity index 69% rename from lib/class-wp-rest-widget-updater-controller.php rename to lib/class-wp-rest-widget-forms.php index 22c8a59bec4e24..c2f7299b557e06 100644 --- a/lib/class-wp-rest-widget-updater-controller.php +++ b/lib/class-wp-rest-widget-forms.php @@ -1,7 +1,7 @@ namespace = 'wp/v2'; - $this->rest_base = 'widgets'; + $this->namespace = '__experimental'; + $this->rest_base = 'widget-forms'; } /** @@ -35,12 +35,24 @@ public function register_routes() { register_rest_route( $this->namespace, // Regex representing a PHP class extracted from http://php.net/manual/en/language.oop5.basic.php. - '/' . $this->rest_base . '/(?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/', + '/' . $this->rest_base . '/(?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/', array( 'args' => array( - 'identifier' => array( - 'description' => __( 'Class name of the widget.', 'gutenberg' ), - 'type' => 'string', + 'widget_class' => array( + 'description' => __( 'Class name of the widget.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + 'validate_callback' => array( $this, 'is_valid_widget' ), + ), + 'instance' => array( + 'description' => __( 'Current widget instance', 'gutenberg' ), + 'type' => 'object', + 'default' => array(), + ), + 'instance_changes' => array( + 'description' => __( 'Array of instance changes', 'gutenberg' ), + 'type' => 'object', + 'default' => array(), ), ), array( @@ -76,51 +88,46 @@ public function compute_new_widget_permissions_check() { } /** - * Returns the new widget instance and the form that represents it. + * Checks if the widget being referenced is valid. * * @since 5.2.0 + * @param string $widget_class Name of the class the widget references. + * + * @return boolean| True if the widget being referenced exists and false otherwise. + */ + private function is_valid_widget( $widget_class ) { + global $wp_widget_factory; + if ( ! $widget_class ) { + return false; + } + return isset( $wp_widget_factory->widgets[ $widget_class ] ) && + ( $wp_widget_factory->widgets[ $widget_class ] instanceof WP_Widget ); + } + + /** + * Returns the new widget instance and the form that represents it. + * + * @since 5.7.0 * @access public * * @param WP_REST_Request $request Full details about the request. * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function compute_new_widget( $request ) { - $widget = $request->get_param( 'identifier' ); + $widget_class = $request->get_param( 'widget_class' ); + $instance = $request->get_param( 'instance' ); + $instance_changes = $request->get_param( 'instance_changes' ); global $wp_widget_factory; + $widget_obj = $wp_widget_factory->widgets[ $widget_class ]; - if ( - null === $widget || - ! isset( $wp_widget_factory->widgets[ $widget ] ) || - ! ( $wp_widget_factory->widgets[ $widget ] instanceof WP_Widget ) - ) { - return new WP_Error( - 'widget_invalid', - __( 'Invalid widget.', 'gutenberg' ), - array( - 'status' => 404, - ) - ); - } - - $widget_obj = $wp_widget_factory->widgets[ $widget ]; - - $instance = $request->get_param( 'instance' ); - if ( null === $instance ) { - $instance = array(); - } - $id_to_use = $request->get_param( 'id_to_use' ); - if ( null === $id_to_use ) { - $id_to_use = -1; - } - - $widget_obj->_set( $id_to_use ); + $widget_obj->_set( -1 ); ob_start(); - $instance_changes = $request->get_param( 'instance_changes' ); - if ( null !== $instance_changes ) { + if ( ! empty( $instance_changes ) ) { $old_instance = $instance; $instance = $widget_obj->update( $instance_changes, $old_instance ); + /** * Filters a widget's settings before saving. * @@ -164,17 +171,12 @@ public function compute_new_widget( $request ) { */ do_action_ref_array( 'in_widget_form', array( &$widget_obj, &$return, $instance ) ); } - - $id_base = $widget_obj->id_base; - $id = $widget_obj->id; - $form = ob_get_clean(); + $form = ob_get_clean(); return rest_ensure_response( array( 'instance' => $instance, 'form' => $form, - 'id_base' => $id_base, - 'id' => $id, ) ); } diff --git a/lib/load.php b/lib/load.php index 58e545b38a6f7a..8eceb1203c7143 100644 --- a/lib/load.php +++ b/lib/load.php @@ -29,8 +29,8 @@ function gutenberg_is_experiment_enabled( $name ) { /** * Start: Include for phase 2 */ - if ( ! class_exists( 'WP_REST_Widget_Updater_Controller' ) ) { - require dirname( __FILE__ ) . '/class-wp-rest-widget-updater-controller.php'; + if ( ! class_exists( 'WP_REST_Widget_Forms' ) ) { + require dirname( __FILE__ ) . '/class-wp-rest-widget-forms.php'; } if ( ! class_exists( 'WP_REST_Widget_Areas_Controller' ) ) { require dirname( __FILE__ ) . '/class-experimental-wp-widget-blocks-manager.php'; diff --git a/lib/rest-api.php b/lib/rest-api.php index 6ad5e6d0e6f0ef..e0b6e7be592e7b 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -63,8 +63,8 @@ function gutenberg_filter_oembed_result( $response, $handler, $request ) { * @since 5.0.0 */ function gutenberg_register_rest_widget_updater_routes() { - $widgets_controller = new WP_REST_Widget_Updater_Controller(); - $widgets_controller->register_routes(); + $widget_forms = new WP_REST_Widget_Forms(); + $widget_forms->register_routes(); } add_action( 'rest_api_init', 'gutenberg_register_rest_widget_updater_routes' ); diff --git a/lib/widgets-page.php b/lib/widgets-page.php index 9ecb3b1139cefd..6762b1f0583712 100644 --- a/lib/widgets-page.php +++ b/lib/widgets-page.php @@ -108,6 +108,7 @@ function gutenberg_widgets_init( $hook ) { ); wp_enqueue_script( 'wp-edit-widgets' ); + wp_enqueue_script( 'admin-widgets' ); wp_enqueue_script( 'wp-format-library' ); wp_enqueue_style( 'wp-edit-widgets' ); wp_enqueue_style( 'wp-format-library' ); diff --git a/lib/widgets.php b/lib/widgets.php index 85a9dea30ce99c..a2dbf475ac1a63 100644 --- a/lib/widgets.php +++ b/lib/widgets.php @@ -63,6 +63,15 @@ function gutenberg_block_editor_admin_print_footer_scripts() { */ function gutenberg_block_editor_admin_footer() { if ( gutenberg_is_block_editor() ) { + // The function wpWidgets.save needs this nonce to work as expected. + echo implode( + "\n", + array( + '
', + wp_nonce_field( 'save-sidebar-widgets', '_wpnonce_widgets', false ), + '
', + ) + ); /** This action is documented in wp-admin/admin-footer.php */ // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores do_action( 'admin_footer-widgets.php' ); @@ -109,32 +118,36 @@ function gutenberg_get_legacy_widget_settings() { if ( ! empty( $wp_widget_factory ) ) { foreach ( $wp_widget_factory->widgets as $class => $widget_obj ) { $available_legacy_widgets[ $class ] = array( - 'name' => html_entity_decode( $widget_obj->name ), + 'name' => html_entity_decode( $widget_obj->name ), // wp_widget_description is not being used because its input parameter is a Widget Id. // Widgets id's reference to a specific widget instance. // Here we are iterating on all the available widget classes even if no widget instance exists for them. - 'description' => isset( $widget_obj->widget_options['description'] ) ? + 'description' => isset( $widget_obj->widget_options['description'] ) ? html_entity_decode( $widget_obj->widget_options['description'] ) : null, - 'isCallbackWidget' => false, - 'isHidden' => in_array( $class, $core_widgets, true ), + 'isReferenceWidget' => false, + 'isHidden' => in_array( $class, $core_widgets, true ), ); } } global $wp_registered_widgets; if ( ! empty( $wp_registered_widgets ) ) { foreach ( $wp_registered_widgets as $widget_id => $widget_obj ) { + + $block_widget_start = 'blocks-widget-'; if ( - is_array( $widget_obj['callback'] ) && + ( is_array( $widget_obj['callback'] ) && isset( $widget_obj['callback'][0] ) && - ( $widget_obj['callback'][0] instanceof WP_Widget ) + ( $widget_obj['callback'][0] instanceof WP_Widget ) ) || + // $widget_id starts with $block_widget_start. + strncmp( $widget_id, $block_widget_start, strlen( $block_widget_start ) ) === 0 ) { continue; } $available_legacy_widgets[ $widget_id ] = array( - 'name' => html_entity_decode( $widget_obj['name'] ), - 'description' => html_entity_decode( wp_widget_description( $widget_id ) ), - 'isCallbackWidget' => true, + 'name' => html_entity_decode( $widget_obj['name'] ), + 'description' => html_entity_decode( wp_widget_description( $widget_id ) ), + 'isReferenceWidget' => true, ); } } @@ -142,7 +155,7 @@ function gutenberg_get_legacy_widget_settings() { $settings['hasPermissionsToManageWidgets'] = $has_permissions_to_manage_widgets; $settings['availableLegacyWidgets'] = $available_legacy_widgets; - return $settings; + return gutenberg_experiments_editor_settings( $settings ); } /** @@ -213,3 +226,12 @@ function gutenberg_create_wp_area_post_type() { add_action( 'init', 'gutenberg_create_wp_area_post_type' ); add_filter( 'sidebars_widgets', 'Experimental_WP_Widget_Blocks_Manager::swap_out_sidebars_blocks_for_block_widgets' ); + +/** + * Function to enqueue admin-widgets as part of the block editor assets. + */ +function gutenberg_enqueue_widget_scripts() { + wp_enqueue_script( 'admin-widgets' ); +} + +add_action( 'enqueue_block_editor_assets', 'gutenberg_enqueue_widget_scripts' ); diff --git a/packages/block-library/src/legacy-widget/edit/dom-manager.js b/packages/block-library/src/legacy-widget/edit/dom-manager.js index d4a0b43829f29c..ccabc4408f0666 100644 --- a/packages/block-library/src/legacy-widget/edit/dom-manager.js +++ b/packages/block-library/src/legacy-widget/edit/dom-manager.js @@ -16,6 +16,8 @@ class LegacyWidgetEditDomManager extends Component { this.containerRef = createRef(); this.formRef = createRef(); this.widgetContentRef = createRef(); + this.idBaseInputRef = createRef(); + this.widgetNumberInputRef = createRef(); this.triggerWidgetEvent = this.triggerWidgetEvent.bind( this ); } @@ -27,11 +29,22 @@ class LegacyWidgetEditDomManager extends Component { } shouldComponentUpdate( nextProps ) { + let shouldTriggerWidgetUpdateEvent = false; // We can not leverage react render otherwise we would destroy dom changes applied by the plugins. // We manually update the required dom node replicating what the widget screen and the customizer do. + if ( nextProps.idBase !== this.props.idBase && this.idBaseInputRef.current ) { + this.idBaseInputRef.current.value = nextProps.idBase; + shouldTriggerWidgetUpdateEvent = true; + } + if ( nextProps.number !== this.props.number && this.widgetNumberInputRef.current ) { + this.widgetNumberInputRef.current.value = nextProps.number; + } if ( nextProps.form !== this.props.form && this.widgetContentRef.current ) { const widgetContent = this.widgetContentRef.current; widgetContent.innerHTML = nextProps.form; + shouldTriggerWidgetUpdateEvent = true; + } + if ( shouldTriggerWidgetUpdateEvent ) { this.triggerWidgetEvent( 'widget-updated' ); this.previousFormData = new window.FormData( this.formRef.current @@ -41,7 +54,7 @@ class LegacyWidgetEditDomManager extends Component { } render() { - const { id, idBase, widgetNumber, form } = this.props; + const { id, idBase, number, form, isReferenceWidget } = this.props; return (
@@ -50,6 +63,11 @@ class LegacyWidgetEditDomManager extends Component { method="post" onBlur={ () => { if ( this.shouldTriggerInstanceUpdate() ) { + if ( isReferenceWidget ) { + if ( this.containerRef.current ) { + window.wpWidgets.save( window.jQuery( this.containerRef.current ) ); + } + } this.props.onInstanceChange( this.retrieveUpdatedInstance() ); @@ -61,11 +79,15 @@ class LegacyWidgetEditDomManager extends Component { className="widget-content" dangerouslySetInnerHTML={ { __html: form } } /> - - - - - + { isReferenceWidget && ( + <> + + + + + + + ) }
@@ -102,20 +124,17 @@ class LegacyWidgetEditDomManager extends Component { } triggerWidgetEvent( event ) { - window.$( window.document ).trigger( + window.jQuery( window.document ).trigger( event, - [ window.$( this.containerRef.current ) ] + [ window.jQuery( this.containerRef.current ) ] ); } retrieveUpdatedInstance() { if ( this.formRef.current ) { - const { idBase, widgetNumber } = this.props; const form = this.formRef.current; const formData = new window.FormData( form ); const updatedInstance = {}; - const keyPrefixLength = `widget-${ idBase }[${ widgetNumber }][`.length; - const keySuffixLength = `]`.length; for ( const rawKey of formData.keys() ) { // This fields are added to the form because the widget JavaScript code may use this values. // They are not relevant for the update mechanism. @@ -125,8 +144,8 @@ class LegacyWidgetEditDomManager extends Component { ) ) { continue; } - const keyParsed = rawKey.substring( keyPrefixLength, rawKey.length - keySuffixLength ); - + const matches = rawKey.match( /[^\[]*\[[-\d]*\]\[([^\]]*)\]/ ); + const keyParsed = matches && matches[ 1 ] ? matches[ 1 ] : rawKey; const value = formData.getAll( rawKey ); if ( value.length > 1 ) { updatedInstance[ keyParsed ] = value; diff --git a/packages/block-library/src/legacy-widget/edit/handler.js b/packages/block-library/src/legacy-widget/edit/handler.js index 157ab653fb89f5..96983ffac1bf29 100644 --- a/packages/block-library/src/legacy-widget/edit/handler.js +++ b/packages/block-library/src/legacy-widget/edit/handler.js @@ -1,8 +1,12 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + /** * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { withInstanceId } from '@wordpress/compose'; @@ -11,13 +15,15 @@ import { withInstanceId } from '@wordpress/compose'; */ import LegacyWidgetEditDomManager from './dom-manager'; +const { XMLHttpRequest, FormData } = window; + class LegacyWidgetEditHandler extends Component { constructor() { super( ...arguments ); this.state = { form: null, - idBase: null, }; + this.widgetNonce = null; this.instanceUpdating = null; this.onInstanceChange = this.onInstanceChange.bind( this ); this.requestWidgetUpdater = this.requestWidgetUpdater.bind( this ); @@ -25,15 +31,23 @@ class LegacyWidgetEditHandler extends Component { componentDidMount() { this.isStillMounted = true; - this.requestWidgetUpdater(); + this.trySetNonce(); + this.requestWidgetUpdater( undefined, ( response ) => { + this.props.onInstanceChange( null, !! response.form ); + } ); } componentDidUpdate( prevProps ) { + if ( ! this.widgetNonce ) { + this.trySetNonce(); + } if ( prevProps.instance !== this.props.instance && this.instanceUpdating !== this.props.instance ) { - this.requestWidgetUpdater(); + this.requestWidgetUpdater( undefined, ( response ) => { + this.props.onInstanceChange( null, !! response.form ); + } ); } if ( this.instanceUpdating === this.props.instance ) { this.instanceUpdating = null; @@ -45,78 +59,133 @@ class LegacyWidgetEditHandler extends Component { } render() { - const { instanceId, identifier } = this.props; - const { id, idBase, form } = this.state; - if ( ! identifier ) { - return __( 'Not a valid widget.' ); - } + const { instanceId, id, number, idBase, instance, isSelected, widgetName } = this.props; + const { form } = this.state; + if ( ! form ) { return null; } + + const widgetTitle = get( instance, [ 'title' ] ); + let title = null; + if ( isSelected ) { + if ( widgetTitle && widgetName ) { + title = `${ widgetName }: ${ widgetTitle }`; + } else if ( ! widgetTitle && widgetName ) { + title = widgetName; + } else if ( widgetTitle && ! widgetName ) { + title = widgetTitle; + } + } return ( -
- { - this.widgetEditDomManagerRef = ref; + <> + { title && ( +
+ { title } +
+ ) } +
-
+ > + + { + this.widgetEditDomManagerRef = ref; + } } + onInstanceChange={ this.onInstanceChange } + number={ number ? number : instanceId * -1 } + id={ id } + idBase={ idBase } + form={ form } + /> +
+ ); } + trySetNonce() { + const element = document.getElementById( '_wpnonce_widgets' ); + if ( element && element.value ) { + this.widgetNonce = element.value; + } + } + onInstanceChange( instanceChanges ) { + const { id } = this.props; + if ( id ) { + // For reference widgets there is no need to query an endpoint, + // the widget is already saved with ajax. + this.props.onInstanceChange( instanceChanges, true ); + return; + } this.requestWidgetUpdater( instanceChanges, ( response ) => { this.instanceUpdating = response.instance; - this.props.onInstanceChange( response.instance ); + this.props.onInstanceChange( response.instance, !! response.form ); } ); } requestWidgetUpdater( instanceChanges, callback ) { - const { identifier, instanceId, instance } = this.props; - if ( ! identifier ) { + const { id, idBase, instance, widgetClass } = this.props; + const { isStillMounted } = this; + if ( ! id && ! widgetClass ) { return; } - apiFetch( { - path: `/wp/v2/widgets/${ identifier }/`, - data: { - identifier, - instance, - // use negative ids to make sure the id does not exist on the database. - id_to_use: instanceId * -1, - instance_changes: instanceChanges, - }, - method: 'POST', - } ).then( - ( response ) => { - if ( this.isStillMounted ) { - this.setState( { - form: response.form, - idBase: response.id_base, - id: response.id, - } ); + // If we are in the presence of a reference widget, do a save ajax request + // with empty changes so we retrieve the widget edit form. + if ( id ) { + const httpRequest = new XMLHttpRequest(); + const formData = new FormData(); + formData.append( 'action', 'save-widget' ); + formData.append( 'id_base', idBase ); + formData.append( 'widget-id', id ); + formData.append( 'widget-width', '250' ); + formData.append( 'widget-height', '200' ); + formData.append( 'savewidgets', this.widgetNonce ); + httpRequest.open( 'POST', window.ajaxurl ); + httpRequest.addEventListener( 'load', () => { + if ( isStillMounted ) { + const form = httpRequest.responseText; + this.setState( { form } ); if ( callback ) { - callback( response ); + callback( { form } ); } } - } - ); + } ); + httpRequest.send( formData ); + return; + } + + if ( widgetClass ) { + apiFetch( { + path: `/__experimental/widget-forms/${ widgetClass }/`, + data: { + instance, + instance_changes: instanceChanges, + }, + method: 'POST', + } ).then( + ( response ) => { + if ( isStillMounted ) { + this.setState( { + form: response.form, + } ); + if ( callback ) { + callback( response ); + } + } + } + ); + } } } export default withInstanceId( LegacyWidgetEditHandler ); - diff --git a/packages/block-library/src/legacy-widget/edit/index.js b/packages/block-library/src/legacy-widget/edit/index.js index 31f36c2f7fac94..3387640fefe872 100644 --- a/packages/block-library/src/legacy-widget/edit/index.js +++ b/packages/block-library/src/legacy-widget/edit/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; + /** * WordPress dependencies */ @@ -26,6 +31,7 @@ class LegacyWidgetEdit extends Component { constructor() { super( ...arguments ); this.state = { + hasEditForm: true, isPreview: false, }; this.switchToEdit = this.switchToEdit.bind( this ); @@ -38,33 +44,38 @@ class LegacyWidgetEdit extends Component { attributes, availableLegacyWidgets, hasPermissionsToManageWidgets, + isSelected, setAttributes, } = this.props; - const { isPreview } = this.state; - const { identifier, isCallbackWidget } = attributes; - const widgetObject = identifier && availableLegacyWidgets[ identifier ]; - if ( ! widgetObject ) { + const { isPreview, hasEditForm } = this.state; + const { id, widgetClass } = attributes; + const widgetObject = + ( id && availableLegacyWidgets[ id ] ) || + ( widgetClass && availableLegacyWidgets[ widgetClass ] ); + if ( ! id && ! widgetClass ) { return ( setAttributes( { - instance: {}, - identifier: newWidget, - isCallbackWidget: availableLegacyWidgets[ newWidget ].isCallbackWidget, - } ) } + onChangeWidget={ ( newWidget ) => { + const { isReferenceWidget } = availableLegacyWidgets[ newWidget ]; + setAttributes( { + instance: {}, + id: isReferenceWidget ? newWidget : undefined, + widgetClass: isReferenceWidget ? undefined : newWidget, + } ); + } } /> ); } - const inspectorControls = ( + const inspectorControls = widgetObject ? ( { widgetObject.description } - ); + ) : null; if ( ! hasPermissionsToManageWidgets ) { return ( <> @@ -78,14 +89,14 @@ class LegacyWidgetEdit extends Component { <> - { ! widgetObject.isHidden && ( + { ( widgetObject && ! widgetObject.isHidden ) && ( ) } - { ! isCallbackWidget && ( + { hasEditForm && ( <>