diff --git a/gutenberg.php b/gutenberg.php
index 4e0cf4bbc32cde..099056a1f00fa8 100644
--- a/gutenberg.php
+++ b/gutenberg.php
@@ -55,6 +55,13 @@ function the_gutenberg_project() {
+
namespace = 'wp/v2';
+ $this->rest_base = 'widget-updater';
+ }
+
+ /**
+ * Registers the necessary REST API route.
+ *
+ * @access public
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/',
+ array(
+ 'args' => array(
+ 'name' => array(
+ 'description' => __( 'Unique registered name for the block.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'compute_new_widget' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns the new widget instance and the form that represents it.
+ *
+ * @since 2.8.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 ) {
+ $json_request = $request->get_json_params();
+ if ( ! isset( $json_request['identifier'] ) ) {
+ return;
+ }
+ $widget = $json_request['identifier'];
+ global $wp_widget_factory;
+
+ if ( ! isset( $wp_widget_factory->widgets[ $widget ] ) ) {
+ return;
+ }
+
+ $widget_obj = $wp_widget_factory->widgets[ $widget ];
+ if ( ! ( $widget_obj instanceof WP_Widget ) ) {
+ return;
+ }
+
+ $instance = isset( $json_request['instance'] ) ? $json_request['instance'] : array();
+
+ $id_to_use = isset( $json_request['id_to_use'] ) ? $json_request['id_to_use'] : -1;
+
+ $widget_obj->_set( $id_to_use );
+ ob_start();
+
+ if ( isset( $json_request['instance_changes'] ) ) {
+ $instance = $widget_obj->update( $json_request['instance_changes'], $instance );
+ // TODO: apply required filters.
+ }
+
+ $widget_obj->form( $instance );
+ // TODO: apply required filters.
+
+ $id_base = $widget_obj->id_base;
+ $id = $widget_obj->id;
+ $form = ob_get_clean();
+
+ return rest_ensure_response(
+ array(
+ 'instance' => $instance,
+ 'form' => $form,
+ 'id_base' => $id_base,
+ 'id' => $id,
+ )
+ );
+ }
+}
diff --git a/lib/client-assets.php b/lib/client-assets.php
index 96986825e9bc27..0c018a9e838d93 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -1112,9 +1112,20 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
);
}
+ $available_legacy_widgets = array();
+ global $wp_widget_factory;
+
+ foreach ( $wp_widget_factory->widgets as $class => $widget_obj ) {
+ $available_legacy_widgets[ $class ] = array(
+ 'name' => html_entity_decode( $widget_obj->name ),
+ 'description' => html_entity_decode( $widget_obj->widget_options['description'] ),
+ );
+ }
+
$editor_settings = array(
'alignWide' => $align_wide || ! empty( $gutenberg_theme_support[0]['wide-images'] ), // Backcompat. Use `align-wide` outside of `gutenberg` array.
'availableTemplates' => $available_templates,
+ 'availableLegacyWidgets' => $available_legacy_widgets,
'allowedBlockTypes' => $allowed_block_types,
'disableCustomColors' => get_theme_support( 'disable-custom-colors' ),
'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ),
diff --git a/lib/load.php b/lib/load.php
index 2f8644ccf4636f..78be355a2ac398 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -12,6 +12,9 @@
// These files only need to be loaded if within a rest server instance
// which this class will exist if that is the case.
if ( class_exists( 'WP_REST_Controller' ) ) {
+ if ( ! class_exists( 'WP_REST_Widget_Updater_Controller' ) ) {
+ require dirname( __FILE__ ) . '/class-wp-rest-widget-updater-controller.php';
+ }
require dirname( __FILE__ ) . '/rest-api.php';
}
@@ -43,9 +46,15 @@
if ( ! function_exists( 'render_block_core_latest_posts' ) ) {
require dirname( __FILE__ ) . '/../packages/block-library/src/latest-posts/index.php';
}
+
+if ( ! function_exists( 'render_block_legacy_widget' ) ) {
+ require dirname( __FILE__ ) . '/../packages/block-library/src/legacy-widget/index.php';
+}
+
if ( ! function_exists( 'render_block_core_rss' ) ) {
require dirname( __FILE__ ) . '/../packages/block-library/src/rss/index.php';
}
if ( ! function_exists( 'render_block_core_shortcode' ) ) {
require dirname( __FILE__ ) . '/../packages/block-library/src/shortcode/index.php';
}
+
diff --git a/lib/rest-api.php b/lib/rest-api.php
index d12d7df12dfcc2..924f1930cc69c3 100644
--- a/lib/rest-api.php
+++ b/lib/rest-api.php
@@ -20,6 +20,17 @@ function gutenberg_register_rest_routes() {
_deprecated_function( __FUNCTION__, '5.0.0' );
}
+/**
+ * Registers the REST API routes needed by the legacy widget block.
+ *
+ * @since 5.0.0
+ */
+function gutenberg_register_rest_widget_updater_routes() {
+ $widgets_controller = new WP_REST_Widget_Updater_Controller();
+ $widgets_controller->register_routes();
+}
+add_action( 'rest_api_init', 'gutenberg_register_rest_widget_updater_routes' );
+
/**
* Handle a failing oEmbed proxy request to try embedding as a shortcode.
*
diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss
index 9fb94f40bdef7c..d6c423cfeac59a 100644
--- a/packages/block-library/src/editor.scss
+++ b/packages/block-library/src/editor.scss
@@ -14,6 +14,7 @@
@import "./image/editor.scss";
@import "./latest-comments/editor.scss";
@import "./latest-posts/editor.scss";
+@import "./legacy-widget/editor.scss";
@import "./media-text/editor.scss";
@import "./list/editor.scss";
@import "./more/editor.scss";
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 5dd3fee7b2a29d..6551cd4e9fa4a5 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -31,6 +31,7 @@ import * as html from './html';
import * as mediaText from './media-text';
import * as latestComments from './latest-comments';
import * as latestPosts from './latest-posts';
+import * as legacyWidget from './legacy-widget';
import * as list from './list';
import * as missing from './missing';
import * as more from './more';
@@ -81,6 +82,7 @@ export const registerCoreBlocks = () => {
mediaText,
latestComments,
latestPosts,
+ legacyWidget,
missing,
more,
nextpage,
diff --git a/packages/block-library/src/legacy-widget/WidgetEditDomManager.js b/packages/block-library/src/legacy-widget/WidgetEditDomManager.js
new file mode 100644
index 00000000000000..db04bbb9be54dd
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/WidgetEditDomManager.js
@@ -0,0 +1,86 @@
+/**
+ * External dependencies
+ */
+import { includes } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component, createRef } from '@wordpress/element';
+
+class WidgetEditDomManager extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.containerRef = createRef();
+ this.triggerWidgetEvent = this.triggerWidgetEvent.bind( this );
+ }
+
+ componentDidMount() {
+ this.triggerWidgetEvent( 'widget-added' );
+ }
+
+ shouldComponentUpdate( nextProps ) {
+ // 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.form !== this.props.form && this.containerRef.current ) {
+ const widgetContent = this.containerRef.current.querySelector( '.widget-content' );
+ widgetContent.innerHTML = nextProps.form;
+ this.triggerWidgetEvent( 'widget-updated' );
+ }
+ return false;
+ }
+
+ render() {
+ const { id, idBase, widgetNumber, form } = this.props;
+ return (
+
+ );
+ }
+
+ triggerWidgetEvent( event ) {
+ window.$( window.document ).trigger(
+ event,
+ [ window.$( this.containerRef.current ) ]
+ );
+ }
+
+ retrieveUpdatedInstance() {
+ if ( this.containerRef.current ) {
+ const { idBase, widgetNumber } = this.props;
+ const form = this.containerRef.current.querySelector( 'form' );
+ const formData = new window.FormData( form );
+ const updatedInstance = {};
+ const keyPrefixLength = `widget-${ idBase }[${ widgetNumber }][`.length;
+ const keySuffixLength = `]`.length;
+ for ( const [ rawKey, value ] of formData ) {
+ const keyParsed = rawKey.substring( keyPrefixLength, rawKey.length - keySuffixLength );
+ // 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' ],
+ keyParsed,
+ ) ) {
+ continue;
+ }
+ updatedInstance[ keyParsed ] = value;
+ }
+ return updatedInstance;
+ }
+ }
+}
+
+export default WidgetEditDomManager;
+
diff --git a/packages/block-library/src/legacy-widget/WidgetEditHandler.js b/packages/block-library/src/legacy-widget/WidgetEditHandler.js
new file mode 100644
index 00000000000000..5082402a61c87d
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/WidgetEditHandler.js
@@ -0,0 +1,120 @@
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import apiFetch from '@wordpress/api-fetch';
+import { withInstanceId } from '@wordpress/compose';
+import { Button } from '@wordpress/components';
+
+import WidgetEditDomManager from './WidgetEditDomManager';
+
+class WidgetEditHandler extends Component {
+ constructor() {
+ super( ...arguments );
+ this.state = {
+ form: null,
+ idBase: null,
+ };
+ this.instanceUpdating = null;
+ this.updateWidget = this.updateWidget.bind( this );
+ this.requestWidgetUpdater = this.requestWidgetUpdater.bind( this );
+ }
+
+ componentDidMount() {
+ this.isStillMounted = true;
+ this.requestWidgetUpdater();
+ }
+
+ componentDidUpdate( prevProps ) {
+ if (
+ prevProps.instance !== this.props.instance &&
+ this.instanceUpdating !== this.props.instance
+ ) {
+ this.requestWidgetUpdater();
+ }
+ if ( this.instanceUpdating === this.props.instance ) {
+ this.instanceUpdating = null;
+ }
+ }
+
+ componentWillUnmount() {
+ this.isStillMounted = false;
+ }
+
+ render() {
+ const { instanceId, identifier } = this.props;
+ const { id, idBase, form } = this.state;
+ if ( ! identifier ) {
+ return __( 'Not a valid widget.' );
+ }
+ if ( ! form ) {
+ return null;
+ }
+ return (
+
+ {
+ this.widgetEditDomManagerRef = ref;
+ } }
+ widgetNumber={ instanceId * -1 }
+ id={ id }
+ idBase={ idBase }
+ form={ form }
+ />
+
+
+ );
+ }
+
+ updateWidget() {
+ if ( this.widgetEditDomManagerRef ) {
+ const instanceChanges = this.widgetEditDomManagerRef.retrieveUpdatedInstance();
+ this.requestWidgetUpdater( instanceChanges, ( response ) => {
+ this.instanceUpdating = response.instance;
+ this.props.onInstanceChange( response.instance );
+ } );
+ }
+ }
+
+ requestWidgetUpdater( instanceChanges, callback ) {
+ const { identifier, instanceId, instance } = this.props;
+ if ( ! identifier ) {
+ return;
+ }
+
+ apiFetch( {
+ path: '/wp/v2/widget-updater/',
+ 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 ( callback ) {
+ callback( response );
+ }
+ }
+ }
+ );
+ }
+}
+
+export default withInstanceId( WidgetEditHandler );
+
diff --git a/packages/block-library/src/legacy-widget/edit.js b/packages/block-library/src/legacy-widget/edit.js
new file mode 100644
index 00000000000000..f75ff174a35423
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/edit.js
@@ -0,0 +1,150 @@
+/**
+ * External dependencies
+ */
+import { map } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component, Fragment } from '@wordpress/element';
+import {
+ Button,
+ IconButton,
+ PanelBody,
+ Placeholder,
+ SelectControl,
+ Toolbar,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { withSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import {
+ BlockControls,
+ BlockIcon,
+ InspectorControls,
+ ServerSideRender,
+} from '@wordpress/editor';
+
+import WidgetEditHandler from './WidgetEditHandler';
+
+class LegacyWidgetEdit extends Component {
+ constructor() {
+ super( ...arguments );
+ this.state = {
+ isPreview: false,
+ };
+ this.switchToEdit = this.switchToEdit.bind( this );
+ this.switchToPreview = this.switchToPreview.bind( this );
+ this.changeWidget = this.changeWidget.bind( this );
+ }
+
+ render() {
+ const { attributes, availableLegacyWidgets, setAttributes } = this.props;
+ const { isPreview } = this.state;
+ const { identifier } = attributes;
+ const widgetObject = identifier && availableLegacyWidgets[ identifier ];
+ if ( ! widgetObject ) {
+ return (
+ }
+ label={ __( 'Legacy Widget' ) }
+ >
+ setAttributes( {
+ instance: {},
+ identifier: value,
+ } ) }
+ options={ [ { value: 'none', label: 'Select widget' } ].concat(
+ map( availableLegacyWidgets, ( widget, key ) => {
+ return {
+ value: key,
+ label: widget.name,
+ };
+ } )
+ ) }
+ />
+
+ );
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+ { widgetObject.description }
+
+
+ { ! isPreview && (
+ {
+ this.props.setAttributes( {
+ instance: newInstance,
+ } );
+ }
+ }
+ />
+ ) }
+ { isPreview && (
+
+ ) }
+
+ );
+ }
+
+ changeWidget() {
+ this.switchToEdit();
+ this.props.setAttributes( {
+ instance: {},
+ identifier: undefined,
+ } );
+ }
+
+ switchToEdit() {
+ this.setState( { isPreview: false } );
+ }
+
+ switchToPreview() {
+ this.setState( { isPreview: true } );
+ }
+}
+
+export default withSelect( ( select ) => {
+ const editorSettings = select( 'core/editor' ).getEditorSettings();
+ const { availableLegacyWidgets } = editorSettings;
+ return {
+ availableLegacyWidgets,
+ };
+} )( LegacyWidgetEdit );
diff --git a/packages/block-library/src/legacy-widget/editor.scss b/packages/block-library/src/legacy-widget/editor.scss
new file mode 100644
index 00000000000000..362f520f8f73ec
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/editor.scss
@@ -0,0 +1,17 @@
+.wp-block-legacy-widget__edit-container,
+.wp-block-legacy-widget__preview {
+ padding-left: 2.5em;
+ padding-right: 2.5em;
+}
+
+.wp-block-legacy-widget__edit-container {
+ .widget-inside {
+ border: none;
+ display: block;
+ }
+}
+
+.wp-block-legacy-widget__update-button {
+ margin-left: auto;
+ display: block;
+}
diff --git a/packages/block-library/src/legacy-widget/index.js b/packages/block-library/src/legacy-widget/index.js
new file mode 100644
index 00000000000000..b70dccc2ff7e32
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/index.js
@@ -0,0 +1,33 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { G, Path, SVG } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+
+export const name = 'core/legacy-widget';
+
+export const settings = {
+ title: __( 'Legacy Widget' ),
+
+ description: __( 'Display a legacy widget.' ),
+
+ icon: ,
+
+ category: 'widgets',
+
+ supports: {
+ html: false,
+ },
+
+ edit,
+
+ save() {
+ // Handled by PHP.
+ return null;
+ },
+};
diff --git a/packages/block-library/src/legacy-widget/index.php b/packages/block-library/src/legacy-widget/index.php
new file mode 100644
index 00000000000000..d1ea7417eff09f
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/index.php
@@ -0,0 +1,46 @@
+ array(
+ 'identifier' => array(
+ 'type' => 'string',
+ ),
+ 'instance' => array(
+ 'type' => 'object',
+ ),
+ ),
+ 'render_callback' => 'render_block_legacy_widget',
+ )
+ );
+}
+
+add_action( 'init', 'register_block_core_legacy_widget' );
diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js
index dc4111ef641d40..de7ca76fd877a4 100644
--- a/packages/editor/src/store/defaults.js
+++ b/packages/editor/src/store/defaults.js
@@ -22,6 +22,7 @@ export const PREFERENCES_DEFAULTS = {
* richEditingEnabled boolean Whether rich editing is enabled or not
*/
export const EDITOR_SETTINGS_DEFAULTS = {
+ availableLegacyWidgets: {},
alignWide: false,
colors: [
{