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: [ {