diff --git a/gutenberg.php b/gutenberg.php index 6253c9a3bf0e48..cbb0375d2a7605 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -46,6 +46,21 @@ function the_gutenberg_project() { + namespace = 'wp/v2'; + $this->rest_base = 'widgets'; + } + + /** + * Registers the necessary REST API route. + * + * @access public + */ + 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]*)/', + array( + 'args' => array( + 'identifier' => array( + 'description' => __( 'Class name of the widget.', 'gutenberg' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'permission_callback' => array( $this, 'compute_new_widget_permissions_check' ), + 'callback' => array( $this, 'compute_new_widget' ), + ), + ) + ); + } + + /** + * Checks if the user has permissions to make the request. + * + * @since 5.2.0 + * @access public + * + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function compute_new_widget_permissions_check() { + // Verify if the current user has edit_theme_options capability. + // This capability is required to access the widgets screen. + if ( ! current_user_can( 'edit_theme_options' ) ) { + return new WP_Error( + 'widgets_cannot_access', + __( 'Sorry, you are not allowed to access widgets on this site.', 'gutenberg' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + return true; + } + + /** + * Returns the new widget instance and the form that represents it. + * + * @since 5.2.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 ) { + $url_params = $request->get_url_params(); + + $widget = $request->get_param( 'identifier' ); + + global $wp_widget_factory; + + 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 ); + ob_start(); + + $instance_changes = $request->get_param( 'instance_changes' ); + if ( null !== $instance_changes ) { + $old_instance = $instance; + $instance = $widget_obj->update( $instance_changes, $old_instance ); + /** + * Filters a widget's settings before saving. + * + * Returning false will effectively short-circuit the widget's ability + * to update settings. The old setting will be returned. + * + * @since 5.2.0 + * + * @param array $instance The current widget instance's settings. + * @param array $instance_changes Array of new widget settings. + * @param array $old_instance Array of old widget settings. + * @param WP_Widget $widget_ob The widget instance. + */ + $instance = apply_filters( 'widget_update_callback', $instance, $instance_changes, $old_instance, $widget_obj ); + if ( false === $instance ) { + $instance = $old_instance; + } + } + + $instance = apply_filters( 'widget_form_callback', $instance, $widget_obj ); + + $return = null; + if ( false !== $instance ) { + $return = $widget_obj->form( $instance ); + + /** + * Fires at the end of the widget control form. + * + * Use this hook to add extra fields to the widget form. The hook + * is only fired if the value passed to the 'widget_form_callback' + * hook is not false. + * + * Note: If the widget has no form, the text echoed from the default + * form method can be hidden using CSS. + * + * @since 5.2.0 + * + * @param WP_Widget $widget_obj The widget instance (passed by reference). + * @param null $return Return null if new fields are added. + * @param array $instance An array of the widget's settings. + */ + 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(); + + return rest_ensure_response( + array( + 'instance' => $instance, + 'form' => $form, + 'id_base' => $id_base, + 'id' => $id, + ) + ); + } +} +/** + * End: Include for phase 2 + */ diff --git a/lib/client-assets.php b/lib/client-assets.php index f721c3855bc0d5..6f000548e0175e 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -872,26 +872,75 @@ function gutenberg_editor_scripts_and_styles( $hook ) { ); } + /** + * Start: Include for phase 2 + */ + + /** + * Todo: The hardcoded array should be replaced with a mechanisms that allows core blocks + * and third party blocks to specify they already have equivalent blocks, and maybe even allow them + * to have a migration function. + */ + $core_widgets = array( 'WP_Widget_Pages', 'WP_Widget_Calendar', 'WP_Widget_Archives', 'WP_Widget_Media_Audio', 'WP_Widget_Media_Image', 'WP_Widget_Media_Gallery', 'WP_Widget_Media_Video', 'WP_Widget_Meta', 'WP_Widget_Search', 'WP_Widget_Text', 'WP_Widget_Categories', 'WP_Widget_Recent_Posts', 'WP_Widget_Recent_Comments', 'WP_Widget_RSS', 'WP_Widget_Tag_Cloud', 'WP_Nav_Menu_Widget', 'WP_Widget_Custom_HTML' ); + + $has_permissions_to_manage_widgets = current_user_can( 'edit_theme_options' ); + $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 ), + 'description' => html_entity_decode( $widget_obj->widget_options['description'] ), + 'isCallbackWidget' => false, + ); + } + } + foreach ( $wp_registered_widgets as $widget_id => $widget_obj ) { + if ( + is_array( $widget_obj['callback'] ) && + isset( $widget_obj['callback'][0] ) && + ( $widget_obj['callback'][0] instanceof WP_Widget ) + ) { + continue; + } + $available_legacy_widgets[ $widget_id ] = array( + 'name' => html_entity_decode( $widget_obj['name'] ), + 'description' => null, + 'isCallbackWidget' => true, + ); + } + /** + * End: Include for phase 2 + */ + $editor_settings = array( - 'alignWide' => $align_wide, - 'availableTemplates' => $available_templates, - 'allowedBlockTypes' => $allowed_block_types, - 'disableCustomColors' => get_theme_support( 'disable-custom-colors' ), - 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ), - 'disablePostFormats' => ! current_theme_supports( 'post-formats' ), - 'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Add title', 'gutenberg' ), $post ), - 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Start writing or type / to choose a block', 'gutenberg' ), $post ), - 'isRTL' => is_rtl(), - 'autosaveInterval' => 10, - 'maxUploadFileSize' => $max_upload_size, - 'allowedMimeTypes' => get_allowed_mime_types(), - 'styles' => $styles, - 'imageSizes' => gutenberg_get_available_image_sizes(), - 'richEditingEnabled' => user_can_richedit(), + 'alignWide' => $align_wide, + 'availableTemplates' => $available_templates, + /** + * Start: Include for phase 2 + */ + 'hasPermissionsToManageWidgets' => $has_permissions_to_manage_widgets, + 'availableLegacyWidgets' => $available_legacy_widgets, + /** + * End: Include for phase 2 + */ + 'allowedBlockTypes' => $allowed_block_types, + 'disableCustomColors' => get_theme_support( 'disable-custom-colors' ), + 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ), + 'disablePostFormats' => ! current_theme_supports( 'post-formats' ), + 'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Add title', 'gutenberg' ), $post ), + 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Start writing or type / to choose a block', 'gutenberg' ), $post ), + 'isRTL' => is_rtl(), + 'autosaveInterval' => 10, + 'maxUploadFileSize' => $max_upload_size, + 'allowedMimeTypes' => get_allowed_mime_types(), + 'styles' => $styles, + 'imageSizes' => gutenberg_get_available_image_sizes(), + 'richEditingEnabled' => user_can_richedit(), // Ideally, we'd remove this and rely on a REST API endpoint. - 'postLock' => $lock_details, - 'postLockUtils' => array( + 'postLock' => $lock_details, + 'postLockUtils' => array( 'nonce' => wp_create_nonce( 'lock-post_' . $post->ID ), 'unlockNonce' => wp_create_nonce( 'update-post_' . $post->ID ), 'ajaxUrl' => admin_url( 'admin-ajax.php' ), @@ -899,7 +948,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) { // Whether or not to load the 'postcustom' meta box is stored as a user meta // field so that we're not always loading its assets. - 'enableCustomFields' => (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true ), + 'enableCustomFields' => (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true ), ); $post_autosave = gutenberg_get_autosave_newer_than_post_save( $post ); diff --git a/lib/load.php b/lib/load.php index 999523ed52ff68..cb6f3c5cee300f 100644 --- a/lib/load.php +++ b/lib/load.php @@ -12,6 +12,15 @@ // 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' ) ) { + /** + * Start: Include for phase 2 + */ + if ( ! class_exists( 'WP_REST_Widget_Updater_Controller' ) ) { + require dirname( __FILE__ ) . '/class-wp-rest-widget-updater-controller.php'; + } + /** + * End: Include for phase 2 + */ require dirname( __FILE__ ) . '/rest-api.php'; } @@ -43,6 +52,18 @@ if ( ! function_exists( 'render_block_core_latest_posts' ) ) { require dirname( __FILE__ ) . '/../packages/block-library/src/latest-posts/index.php'; } + + +/** + * Start: Include for phase 2 + */ +if ( ! function_exists( 'render_block_legacy_widget' ) ) { + require dirname( __FILE__ ) . '/../packages/block-library/src/legacy-widget/index.php'; +} +/** + * End: Include for phase 2 + */ + if ( ! function_exists( 'render_block_core_rss' ) ) { require dirname( __FILE__ ) . '/../packages/block-library/src/rss/index.php'; } diff --git a/lib/rest-api.php b/lib/rest-api.php index 1b36ba7df9b38b..eaccc09c61978d 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -51,3 +51,22 @@ function gutenberg_filter_oembed_result( $response, $handler, $request ) { ); } add_filter( 'rest_request_after_callbacks', 'gutenberg_filter_oembed_result', 10, 3 ); + + + +/** + * Start: Include for phase 2 + */ +/** + * 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' ); +/** + * End: Include for phase 2 + */ diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 3172cb260f2a83..87f2bfb49d5dda 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -28,20 +28,22 @@ Undocumented declaration. The default editor settings - alignWide boolean Enable/Disable Wide/Full Alignments - colors Array Palette colors - disableCustomColors boolean Whether or not the custom colors are disabled - fontSizes Array Available font sizes - disableCustomFontSizes boolean Whether or not the custom font sizes are disabled - imageSizes Array Available image sizes - maxWidth number Max width to constraint resizing - allowedBlockTypes boolean|Array Allowed block types - hasFixedToolbar boolean Whether or not the editor toolbar is fixed - focusMode boolean Whether the focus mode is enabled or not - styles Array Editor Styles - isRTL boolean Whether the editor is in RTL mode - bodyPlaceholder string Empty post placeholder - titlePlaceholder string Empty title placeholder + alignWide boolean Enable/Disable Wide/Full Alignments + availableLegacyWidgets Array Array of objects representing the legacy widgets available. + colors Array Palette colors + disableCustomColors boolean Whether or not the custom colors are disabled + fontSizes Array Available font sizes + disableCustomFontSizes boolean Whether or not the custom font sizes are disabled + imageSizes Array Available image sizes + maxWidth number Max width to constraint resizing + allowedBlockTypes boolean|Array Allowed block types + hasFixedToolbar boolean Whether or not the editor toolbar is fixed + hasPermissionsToManageWidgets boolean Whether or not the user is able to manage widgets. + focusMode boolean Whether the focus mode is enabled or not + styles Array Editor Styles + isRTL boolean Whether the editor is in RTL mode + bodyPlaceholder string Empty post placeholder + titlePlaceholder string Empty title placeholder diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index 728b500a51d593..14c114216f12a9 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -10,20 +10,22 @@ export const PREFERENCES_DEFAULTS = { /** * The default editor settings * - * alignWide boolean Enable/Disable Wide/Full Alignments - * colors Array Palette colors - * disableCustomColors boolean Whether or not the custom colors are disabled - * fontSizes Array Available font sizes - * disableCustomFontSizes boolean Whether or not the custom font sizes are disabled - * imageSizes Array Available image sizes - * maxWidth number Max width to constraint resizing - * allowedBlockTypes boolean|Array Allowed block types - * hasFixedToolbar boolean Whether or not the editor toolbar is fixed - * focusMode boolean Whether the focus mode is enabled or not - * styles Array Editor Styles - * isRTL boolean Whether the editor is in RTL mode - * bodyPlaceholder string Empty post placeholder - * titlePlaceholder string Empty title placeholder + * alignWide boolean Enable/Disable Wide/Full Alignments + * availableLegacyWidgets Array Array of objects representing the legacy widgets available. + * colors Array Palette colors + * disableCustomColors boolean Whether or not the custom colors are disabled + * fontSizes Array Available font sizes + * disableCustomFontSizes boolean Whether or not the custom font sizes are disabled + * imageSizes Array Available image sizes + * maxWidth number Max width to constraint resizing + * allowedBlockTypes boolean|Array Allowed block types + * hasFixedToolbar boolean Whether or not the editor toolbar is fixed + * hasPermissionsToManageWidgets boolean Whether or not the user is able to manage widgets. + * focusMode boolean Whether the focus mode is enabled or not + * styles Array Editor Styles + * isRTL boolean Whether the editor is in RTL mode + * bodyPlaceholder string Empty post placeholder + * titlePlaceholder string Empty title placeholder */ export const SETTINGS_DEFAULTS = { alignWide: false, @@ -131,5 +133,8 @@ export const SETTINGS_DEFAULTS = { // List of allowed mime types and file extensions. allowedMimeTypes: null, + + availableLegacyWidgets: {}, + hasPermissionsToManageWidgets: false, }; diff --git a/packages/block-library/README.md b/packages/block-library/README.md index f21ffbea41303d..78573bbf753e7d 100644 --- a/packages/block-library/README.md +++ b/packages/block-library/README.md @@ -18,7 +18,7 @@ _This package assumes that your code will run in an **ES2015+** environment. If ### registerCoreBlocks -[src/index.js#L69-L130](src/index.js#L69-L130) +[src/index.js#L70-L132](src/index.js#L70-L132) Function to register core blocks provided by the block editor. diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 517218be5e7154..8422d2c82571de 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 1fc00265bc8759..81a0e9bec632b4 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -34,6 +34,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'; @@ -97,6 +98,7 @@ export const registerCoreBlocks = () => { mediaText, latestComments, latestPosts, + process.env.GUTENBERG_PHASE === 2 ? legacyWidget : null, 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..30ded7a7a7a6f8 --- /dev/null +++ b/packages/block-library/src/legacy-widget/WidgetEditDomManager.js @@ -0,0 +1,143 @@ +/** + * External dependencies + */ +import { includes } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component, createRef } from '@wordpress/element'; +import isShallowEqual from '@wordpress/is-shallow-equal'; + +class WidgetEditDomManager extends Component { + constructor() { + super( ...arguments ); + + this.containerRef = createRef(); + this.formRef = createRef(); + this.widgetContentRef = createRef(); + this.triggerWidgetEvent = this.triggerWidgetEvent.bind( this ); + } + + componentDidMount() { + this.triggerWidgetEvent( 'widget-added' ); + this.previousFormData = new window.FormData( + this.formRef.current + ); + } + + 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.widgetContentRef.current ) { + const widgetContent = this.widgetContentRef.current; + widgetContent.innerHTML = nextProps.form; + this.triggerWidgetEvent( 'widget-updated' ); + this.previousFormData = new window.FormData( + this.formRef.current + ); + } + return false; + } + + render() { + const { id, idBase, widgetNumber, form } = this.props; + return ( +
+
+
{ + if ( this.shouldTriggerInstanceUpdate() ) { + this.props.onInstanceChange( + this.retrieveUpdatedInstance() + ); + } + } } + > +
+ + + + + + +
+
+ ); + } + + shouldTriggerInstanceUpdate() { + if ( ! this.formRef.current ) { + return false; + } + if ( ! this.previousFormData ) { + return true; + } + const currentFormData = new window.FormData( + this.formRef.current + ); + const currentFormDataKeys = Array.from( currentFormData.keys() ); + const previousFormDataKeys = Array.from( this.previousFormData.keys() ); + if ( + currentFormDataKeys.length !== previousFormDataKeys.length + ) { + return true; + } + for ( const rawKey of currentFormDataKeys ) { + if ( ! isShallowEqual( + currentFormData.getAll( rawKey ), + this.previousFormData.getAll( rawKey ) + ) ) { + this.previousFormData = currentFormData; + return true; + } + } + return false; + } + + triggerWidgetEvent( event ) { + window.$( window.document ).trigger( + event, + [ window.$( 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. + if ( includes( + [ 'widget-id', 'id_base', 'widget_number', 'multi_number', 'add_new' ], + rawKey, + ) ) { + continue; + } + const keyParsed = rawKey.substring( keyPrefixLength, rawKey.length - keySuffixLength ); + + const value = formData.getAll( rawKey ); + if ( value.length > 1 ) { + updatedInstance[ keyParsed ] = value; + } else { + updatedInstance[ keyParsed ] = value[ 0 ]; + } + } + 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..527e864c90037a --- /dev/null +++ b/packages/block-library/src/legacy-widget/WidgetEditHandler.js @@ -0,0 +1,122 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; +import { withInstanceId } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import WidgetEditDomManager from './WidgetEditDomManager'; + +class WidgetEditHandler extends Component { + constructor() { + super( ...arguments ); + this.state = { + form: null, + idBase: null, + }; + this.instanceUpdating = null; + this.onInstanceChange = this.onInstanceChange.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; + } } + onInstanceChange={ this.onInstanceChange } + widgetNumber={ instanceId * -1 } + id={ id } + idBase={ idBase } + form={ form } + /> +
+ ); + } + + onInstanceChange( instanceChanges ) { + 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/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 ( 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..e2f2a4c4cf2405 --- /dev/null +++ b/packages/block-library/src/legacy-widget/edit.js @@ -0,0 +1,195 @@ +/** + * 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, + hasPermissionsToManageWidgets, + setAttributes, + } = this.props; + const { isPreview } = this.state; + const { identifier, isCallbackWidget } = attributes; + const widgetObject = identifier && availableLegacyWidgets[ identifier ]; + if ( ! widgetObject ) { + let placeholderContent; + + if ( ! hasPermissionsToManageWidgets ) { + placeholderContent = __( 'You don\'t have permissions to use widgets on this site.' ); + } else if ( availableLegacyWidgets.length === 0 ) { + placeholderContent = __( 'There are no widgets available.' ); + } else { + placeholderContent = ( + setAttributes( { + instance: {}, + identifier: value, + isCallbackWidget: availableLegacyWidgets[ value ].isCallbackWidget, + } ) } + options={ [ { value: 'none', label: 'Select widget' } ].concat( + map( availableLegacyWidgets, ( widget, key ) => { + return { + value: key, + label: widget.name, + }; + } ) + ) } + /> + ); + } + + return ( + } + label={ __( 'Legacy Widget' ) } + > + { placeholderContent } + + ); + } + + const inspectorControls = ( + + + { widgetObject.description } + + + ); + if ( ! hasPermissionsToManageWidgets ) { + return ( + + { inspectorControls } + { this.renderWidgetPreview() } + + ); + } + + return ( + + + + + + { ! isCallbackWidget && ( + + + + + ) } + + + { inspectorControls } + { ! isCallbackWidget && ( + { + this.props.setAttributes( { + instance: newInstance, + } ); + } + } + /> + ) } + { ( isPreview || isCallbackWidget ) && this.renderWidgetPreview() } + + ); + } + + changeWidget() { + this.switchToEdit(); + this.props.setAttributes( { + instance: {}, + identifier: undefined, + } ); + } + + switchToEdit() { + this.setState( { isPreview: false } ); + } + + switchToPreview() { + this.setState( { isPreview: true } ); + } + + renderWidgetPreview() { + const { attributes } = this.props; + return ( + + ); + } +} + +export default withSelect( ( select ) => { + const editorSettings = select( 'core/block-editor' ).getSettings(); + const { + availableLegacyWidgets, + hasPermissionsToManageWidgets, + } = editorSettings; + return { + hasPermissionsToManageWidgets, + 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..f3b8b13b1e1f96 --- /dev/null +++ b/packages/block-library/src/legacy-widget/editor.scss @@ -0,0 +1,25 @@ +.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; +} + +.wp-block-legacy-widget__edit-container .widget-inside { + box-shadow: none; +} + +.wp-block-legacy-widget__preview { + overflow: auto; +} 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..43142ec5ca1306 --- /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 (Experimental)' ), + + 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..b812f8e3c710ff --- /dev/null +++ b/packages/block-library/src/legacy-widget/index.php @@ -0,0 +1,78 @@ + $identifier, + 'widget_name' => $widget['name'], + ), + (array) $wp_registered_widgets[ $identifier ]['params'] + ); + $params = apply_filters( 'dynamic_sidebar_params', $params ); + + $callback = $widget['callback']; + + if ( is_callable( $callback ) ) { + ob_start(); + call_user_func_array( $callback, $params ); + return ob_get_clean(); + } + return ''; + } + ob_start(); + the_widget( $identifier, $attributes['instance'] ); + return ob_get_clean(); + +} + +/** + * Register legacy widget block. + */ +function register_block_core_legacy_widget() { + register_block_type( + 'core/legacy-widget', + array( + 'attributes' => array( + 'identifier' => array( + 'type' => 'string', + ), + 'instance' => array( + 'type' => 'object', + ), + 'isCallbackWidget' => array( + 'type' => 'boolean', + ), + ), + 'render_callback' => 'render_block_legacy_widget', + ) + ); +} + +add_action( 'init', 'register_block_core_legacy_widget' ); diff --git a/packages/e2e-tests/fixtures/blocks/core__legacy-widget.html b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.html new file mode 100644 index 00000000000000..a94242f7e4e6ea --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.html @@ -0,0 +1 @@ + diff --git a/packages/e2e-tests/fixtures/blocks/core__legacy-widget.json b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.json new file mode 100644 index 00000000000000..c9e750dbaa0663 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.json @@ -0,0 +1,10 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/legacy-widget", + "isValid": true, + "attributes": {}, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__legacy-widget.parsed.json b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.parsed.json new file mode 100644 index 00000000000000..93ea4bba8f175d --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.parsed.json @@ -0,0 +1,18 @@ +[ + { + "blockName": "core/legacy-widget", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__legacy-widget.serialized.html b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.serialized.html new file mode 100644 index 00000000000000..a94242f7e4e6ea --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.serialized.html @@ -0,0 +1 @@ + diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 8817eb5d4be3b3..14ead7919501f8 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -55,6 +55,7 @@ class EditorProvider extends Component { return { ...pick( settings, [ 'alignWide', + 'availableLegacyWidgets', 'colors', 'disableCustomColors', 'fontSizes', @@ -63,6 +64,7 @@ class EditorProvider extends Component { 'maxWidth', 'allowedBlockTypes', 'hasFixedToolbar', + 'hasPermissionsToManageWidgets', 'focusMode', 'styles', 'isRTL',