diff --git a/lib/class-experimental-wp-widget-blocks-manager.php b/lib/class-experimental-wp-widget-blocks-manager.php index f2f26e5ef4372..4188bb29cb561 100644 --- a/lib/class-experimental-wp-widget-blocks-manager.php +++ b/lib/class-experimental-wp-widget-blocks-manager.php @@ -177,7 +177,7 @@ private static function get_widget_class( $widget_id ) { * @param string $id Idenfitier of the widget instance. * @return array Array containing the widget instance. */ - private static function get_sidebar_widget_instance( $sidebar, $id ) { + public static function get_sidebar_widget_instance( $sidebar, $id ) { list( $object, $number, $name ) = self::get_widget_info( $id ); if ( ! $object ) { return array(); diff --git a/lib/class-wp-rest-widget-updater-controller.php b/lib/class-wp-rest-widget-updater-controller.php index 23c46887ba573..7fbde9f1b3bd0 100644 --- a/lib/class-wp-rest-widget-updater-controller.php +++ b/lib/class-wp-rest-widget-updater-controller.php @@ -34,13 +34,30 @@ public function __construct() { 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 . '/', array( 'args' => array( - 'identifier' => array( + 'widget_class' => array( 'description' => __( 'Class name of the widget.', 'gutenberg' ), 'type' => 'string', + 'required' => false, + 'default' => null, + ), + 'identifier' => array( + 'description' => __( 'Identifier of the widget.', 'gutenberg' ), + 'type' => 'string', + 'required' => false, + 'default' => null, + ), + '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,53 +93,120 @@ 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 - * @access public + * @param string $identifier Instance identifier of the widget. + * @param string $widget_class Name of the class the widget references. * - * @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. + * @return boolean True if the widget being referenced exists and false otherwise. */ - public function compute_new_widget( $request ) { - $url_params = $request->get_url_params(); - - $widget = $request->get_param( 'identifier' ); + private function is_valid_widget( $identifier, $widget_class ) { + global $wp_widget_factory, $wp_registered_widgets; + if ( ! $identifier && ! $widget_class ) { + return false; + } + if ( $identifier ) { + return isset( $wp_registered_widgets[ $identifier ] ); + } + return isset( $wp_widget_factory->widgets[ $widget_class ] ) && + ( $wp_widget_factory->widgets[ $widget_class ] instanceof WP_Widget ); + } - global $wp_widget_factory; + /** + * Computes an array with instance changes cleaned of widget specific prefixes and sufixes. + * + * @since 5.7.0 + * @param string $id_base Widget ID Base. + * @param string $id Widget instance identifier. + * @param array $instance_changes Array with the form values being being changed. + * + * @return array An array based on $instance_changes whose keys have the widget specific sufixes and prefixes removed. + */ + private function parse_instance_changes( $id_base, $id, $instance_changes ) { + $instance_changes_parsed = array(); + $start_position = strlen( 'widget-' . $id_base . '[' . $id . '][' ); + foreach ( $instance_changes as $key => $value ) { + $key_parsed = substr( $key, $start_position, -1 ); + $instance_changes_parsed[ $key_parsed ] = $value; + } + return $instance_changes_parsed; + } + /** + * Returns the edit form of the widget being referenced. + * + * @since 5.7.0 + * @param string $identifier Instance identifier of the widget. + * + * @return WP_REST_Response Response object. + */ + private function handle_reference_widgets( $identifier ) { + global $wp_registered_widget_controls; + $form = ''; + $id_base = $identifier; + $id = $identifier; + $number = null; if ( - null === $widget || - ! isset( $wp_widget_factory->widgets[ $widget ] ) || - ! ( $wp_widget_factory->widgets[ $widget ] instanceof WP_Widget ) + isset( $wp_registered_widget_controls[ $identifier ]['callback'] ) && + is_callable( $wp_registered_widget_controls[ $identifier ]['callback'] ) ) { - return new WP_Error( - 'widget_invalid', - __( 'Invalid widget.', 'gutenberg' ), - array( - 'status' => 404, - ) - ); + $control = $wp_registered_widget_controls[ $identifier ]; + ob_start(); + call_user_func_array( $control['callback'], $control['params'] ); + $form = ob_get_clean(); + if ( isset( $control['id_base'] ) ) { + $id_base = $control['id_base']; + } + if ( isset( $control['params'][0]['number'] ) ) { + $number = $control['params'][0]['number']; + } } - $widget_obj = $wp_widget_factory->widgets[ $widget ]; + return rest_ensure_response( + array( + 'instance' => array(), + 'form' => $form, + 'id_base' => $id_base, + 'id' => $id, + 'number' => $number, + ) + ); + } - $instance = $request->get_param( 'instance' ); + /** + * Returns the new class widget instance and the form that represents it. + * + * @since 5.7.0 + * @access public + * + * @param string $widget_class Widget id for callback widgets or widget class name for class widgets. + * @param array $instance Previous widget instance. + * @param array $instance_changes Array with the form values being being changed. + * @param string $id_to_use Identifier of the specific widget instance. + * @return WP_REST_Response Response object on success, or WP_Error object on failure. + */ + private function handle_class_widgets( $widget_class, $instance, $instance_changes, $id_to_use ) { if ( null === $instance ) { $instance = array(); } - $id_to_use = $request->get_param( 'id_to_use' ); if ( null === $id_to_use ) { $id_to_use = -1; } + global $wp_widget_factory; + $widget_obj = $wp_widget_factory->widgets[ $widget_class ]; + $widget_obj->_set( $id_to_use ); + $id_base = $widget_obj->id_base; + $id = $widget_obj->id; ob_start(); - $instance_changes = $request->get_param( 'instance_changes' ); if ( null !== $instance_changes ) { - $old_instance = $instance; - $instance = $widget_obj->update( $instance_changes, $old_instance ); + $instance_changes = $this->parse_instance_changes( $id_base, $id_to_use, $instance_changes ); + $old_instance = $instance; + $instance = $widget_obj->update( $instance_changes, $old_instance ); + /** * Filters a widget's settings before saving. * @@ -166,10 +250,7 @@ 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( @@ -177,9 +258,44 @@ public function compute_new_widget( $request ) { 'form' => $form, 'id_base' => $id_base, 'id' => $id, + 'number' => $id_to_use, ) ); } + + /** + * 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 ) { + $identifier = $request->get_param( 'identifier' ); + $widget_class = $request->get_param( 'widget_class' ); + + if ( ! $this->is_valid_widget( $identifier, $widget_class ) ) { + return new WP_Error( + 'widget_invalid', + __( 'Invalid widget.', 'gutenberg' ), + array( + 'status' => 404, + ) + ); + } + + if ( $identifier ) { + return $this->handle_reference_widgets( $identifier ); + } + return $this->handle_class_widgets( + $widget_class, + $request->get_param( 'instance' ), + $request->get_param( 'instance_changes' ), + $request->get_param( 'id_to_use' ) + ); + } } /** * End: Include for phase 2 diff --git a/lib/load.php b/lib/load.php index d93b9dc1f9f0e..1c9a4fb84cc42 100644 --- a/lib/load.php +++ b/lib/load.php @@ -20,6 +20,8 @@ } if ( ! class_exists( 'WP_REST_Widget_Areas_Controller' ) ) { require dirname( __FILE__ ) . '/class-experimental-wp-widget-blocks-manager.php'; + } + if ( ! class_exists( 'WP_REST_Widget_Areas_Controller' ) ) { require dirname( __FILE__ ) . '/class-wp-rest-widget-areas-controller.php'; } /** diff --git a/lib/widgets.php b/lib/widgets.php index 28ebc00367475..865f928faa2dc 100644 --- a/lib/widgets.php +++ b/lib/widgets.php @@ -58,6 +58,9 @@ function gutenberg_block_editor_admin_print_footer_scripts() { */ function gutenberg_block_editor_admin_footer() { if ( gutenberg_is_block_editor() ) { + echo '
'; + echo wp_nonce_field( 'save-sidebar-widgets', '_wpnonce_widgets', false ); + echo '
'; /** This action is documented in wp-admin/admin-footer.php */ // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores do_action( 'admin_footer-widgets.php' ); @@ -102,18 +105,17 @@ function gutenberg_get_legacy_widget_settings() { $available_legacy_widgets = array(); global $wp_widget_factory, $wp_registered_widgets; foreach ( $wp_widget_factory->widgets as $class => $widget_obj ) { - if ( ! in_array( $class, $core_widgets ) ) { - $available_legacy_widgets[ $class ] = array( - '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'] ) ? - html_entity_decode( $widget_obj->widget_options['description'] ) : - null, - 'isCallbackWidget' => false, - ); - } + $available_legacy_widgets[ $class ] = array( + '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'] ) ? + html_entity_decode( $widget_obj->widget_options['description'] ) : + null, + 'isCallbackWidget' => false, + 'isHidden' => in_array( $class, $core_widgets, true ), + ); } foreach ( $wp_registered_widgets as $widget_id => $widget_obj ) { if ( @@ -204,3 +206,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 d4a0b43829f29..659ee07564b94 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.widgetNumber !== this.props.widgetNumber && this.widgetNumberInputRef.current ) { + this.widgetNumberInputRef.current.value = nextProps.widgetNumber; + } 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, widgetNumber, form, identifier } = this.props; return (
@@ -50,6 +63,11 @@ class LegacyWidgetEditDomManager extends Component { method="post" onBlur={ () => { if ( this.shouldTriggerInstanceUpdate() ) { + if ( identifier ) { + if ( this.containerRef.current ) { + window.wpWidgets.save( window.$( this.containerRef.current ) ); + } + } this.props.onInstanceChange( this.retrieveUpdatedInstance() ); @@ -62,8 +80,8 @@ class LegacyWidgetEditDomManager extends Component { dangerouslySetInnerHTML={ { __html: form } } /> - - + + @@ -110,28 +128,24 @@ class LegacyWidgetEditDomManager extends Component { 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() ) { + for ( const key 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. if ( includes( [ 'widget-id', 'id_base', 'widget_number', 'multi_number', 'add_new' ], - rawKey, + key, ) ) { continue; } - const keyParsed = rawKey.substring( keyPrefixLength, rawKey.length - keySuffixLength ); - const value = formData.getAll( rawKey ); + const value = formData.getAll( key ); if ( value.length > 1 ) { - updatedInstance[ keyParsed ] = value; + updatedInstance[ key ] = value; } else { - updatedInstance[ keyParsed ] = value[ 0 ]; + updatedInstance[ key ] = value[ 0 ]; } } return updatedInstance; diff --git a/packages/block-library/src/legacy-widget/edit/handler.js b/packages/block-library/src/legacy-widget/edit/handler.js index 157ab653fb89f..347eb96792f83 100644 --- a/packages/block-library/src/legacy-widget/edit/handler.js +++ b/packages/block-library/src/legacy-widget/edit/handler.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { withInstanceId } from '@wordpress/compose'; @@ -17,6 +16,7 @@ class LegacyWidgetEditHandler extends Component { this.state = { form: null, idBase: null, + widgetNumber: null, }; this.instanceUpdating = null; this.onInstanceChange = this.onInstanceChange.bind( this ); @@ -25,7 +25,9 @@ class LegacyWidgetEditHandler extends Component { componentDidMount() { this.isStillMounted = true; - this.requestWidgetUpdater(); + this.requestWidgetUpdater( undefined, ( response ) => { + this.props.onInstanceChange( null, !! response.form ); + } ); } componentDidUpdate( prevProps ) { @@ -33,7 +35,9 @@ class LegacyWidgetEditHandler extends Component { 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; @@ -46,10 +50,8 @@ class LegacyWidgetEditHandler extends Component { render() { const { instanceId, identifier } = this.props; - const { id, idBase, form } = this.state; - if ( ! identifier ) { - return __( 'Not a valid widget.' ); - } + const { id, idBase, form, widgetNumber } = this.state; + if ( ! form ) { return null; } @@ -69,10 +71,11 @@ class LegacyWidgetEditHandler extends Component { this.widgetEditDomManagerRef = ref; } } onInstanceChange={ this.onInstanceChange } - widgetNumber={ instanceId * -1 } + widgetNumber={ widgetNumber ? widgetNumber : instanceId * -1 } id={ id } idBase={ idBase } form={ form } + identifier={ identifier } />
); @@ -81,23 +84,24 @@ class LegacyWidgetEditHandler extends Component { onInstanceChange( instanceChanges ) { 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 { identifier, instanceId, instance, widgetClass } = this.props; + if ( ! identifier && ! widgetClass ) { return; } apiFetch( { - path: `/wp/v2/widgets/${ identifier }/`, + path: `/wp/v2/widgets/`, data: { identifier, instance, // use negative ids to make sure the id does not exist on the database. id_to_use: instanceId * -1, + widget_class: widgetClass, instance_changes: instanceChanges, }, method: 'POST', @@ -108,6 +112,7 @@ class LegacyWidgetEditHandler extends Component { form: response.form, idBase: response.id_base, id: response.id, + widgetNumber: response.number, } ); if ( callback ) { callback( response ); diff --git a/packages/block-library/src/legacy-widget/edit/index.js b/packages/block-library/src/legacy-widget/edit/index.js index 4b645318eb227..7c51e87e57416 100644 --- a/packages/block-library/src/legacy-widget/edit/index.js +++ b/packages/block-library/src/legacy-widget/edit/index.js @@ -1,7 +1,10 @@ /** * External dependencies */ -import { map } from 'lodash'; +import { + map, + pickBy, +} from 'lodash'; /** * WordPress dependencies @@ -33,6 +36,7 @@ class LegacyWidgetEdit extends Component { constructor() { super( ...arguments ); this.state = { + hasEditForm: true, isPreview: false, }; this.switchToEdit = this.switchToEdit.bind( this ); @@ -47,28 +51,35 @@ class LegacyWidgetEdit extends Component { hasPermissionsToManageWidgets, setAttributes, } = this.props; - const { isPreview } = this.state; - const { identifier, isCallbackWidget } = attributes; + const visibleLegacyWidgets = pickBy( + availableLegacyWidgets, + ( { isHidden } ) => ! isHidden + ); + const { isPreview, hasEditForm } = this.state; + const { identifier, widgetClass } = attributes; const widgetObject = identifier && availableLegacyWidgets[ identifier ]; - if ( ! widgetObject ) { + if ( ! identifier && ! widgetClass ) { let placeholderContent; if ( ! hasPermissionsToManageWidgets ) { placeholderContent = __( 'You don\'t have permissions to use widgets on this site.' ); - } else if ( availableLegacyWidgets.length === 0 ) { + } else if ( visibleLegacyWidgets.length === 0 ) { placeholderContent = __( 'There are no widgets available.' ); } else { placeholderContent = ( setAttributes( { - instance: {}, - identifier: value, - isCallbackWidget: availableLegacyWidgets[ value ].isCallbackWidget, - } ) } + onChange={ ( value ) => { + const { isCallbackWidget } = visibleLegacyWidgets[ value ]; + setAttributes( { + instance: {}, + identifier: isCallbackWidget ? value : undefined, + widgetClass: isCallbackWidget ? undefined : value, + } ); + } } options={ [ { value: 'none', label: 'Select widget' } ].concat( - map( availableLegacyWidgets, ( widget, key ) => { + map( visibleLegacyWidgets, ( widget, key ) => { return { value: key, label: widget.name, @@ -89,13 +100,13 @@ class LegacyWidgetEdit extends Component { ); } - const inspectorControls = ( + const inspectorControls = widgetObject ? ( { widgetObject.description } - ); + ) : null; if ( ! hasPermissionsToManageWidgets ) { return ( <> @@ -115,7 +126,7 @@ class LegacyWidgetEdit extends Component { icon="update" > - { ! isCallbackWidget && ( + { hasEditForm && ( <>