diff --git a/lib/class-experimental-wp-widget-blocks-manager.php b/lib/class-experimental-wp-widget-blocks-manager.php index f2f26e5ef4372..82e9ef0f8b12b 100644 --- a/lib/class-experimental-wp-widget-blocks-manager.php +++ b/lib/class-experimental-wp-widget-blocks-manager.php @@ -308,9 +308,14 @@ public static function output_blocks_widget( $options, $arguments ) { } /** - * Registers of a widget that should represent a set of blocks and returns its id. + * Noop block widget control output function for the necessary call to `wp_register_widget_control`. + */ + public static function output_blocks_widget_control() {} + + /** + * Registers a widget that should represent a set of blocks and returns its ID. * - * @param array $blocks Array of blocks. + * @param array $blocks Array of blocks. */ public static function convert_blocks_to_widget( $blocks ) { $widget_id = 'blocks-widget-' . md5( self::serialize_blocks( $blocks ) ); @@ -320,7 +325,7 @@ public static function convert_blocks_to_widget( $blocks ) { } wp_register_sidebar_widget( $widget_id, - __( 'Blocks Area ', 'gutenberg' ), + __( 'Blocks Area', 'gutenberg' ), 'Experimental_WP_Widget_Blocks_Manager::output_blocks_widget', array( 'classname' => 'widget-area', @@ -330,6 +335,12 @@ public static function convert_blocks_to_widget( $blocks ) { 'blocks' => $blocks, ) ); + wp_register_widget_control( + $widget_id, + __( 'Blocks Area', 'gutenberg' ), + 'Experimental_WP_Widget_Blocks_Manager::output_blocks_widget_control', + array( 'id_base' => 'blocks-widget' ) + ); return $widget_id; } @@ -340,20 +351,34 @@ public static function convert_blocks_to_widget( $blocks ) { */ public static function swap_out_sidebars_blocks_for_block_widgets( $sidebars_widgets_input ) { global $sidebars_widgets; + global $wp_customize; if ( null === self::$unfiltered_sidebar_widgets ) { self::$unfiltered_sidebar_widgets = $sidebars_widgets; } + $changeset_data = null; + if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) { + $changeset_data = $wp_customize->changeset_data(); + if ( isset( $changeset_data['gutenberg_widget_blocks']['value'] ) ) { + $changeset_data = json_decode( $changeset_data['gutenberg_widget_blocks']['value'] ); + } + } + $filtered_sidebar_widgets = array(); foreach ( $sidebars_widgets_input as $sidebar_id => $item ) { - if ( ! is_numeric( $item ) ) { + $changeset_value = $changeset_data && isset( $changeset_data->$sidebar_id ) + ? $changeset_data->$sidebar_id + : null; + + if ( ! is_numeric( $item ) && ! $changeset_value ) { $filtered_sidebar_widgets[ $sidebar_id ] = $item; continue; } $filtered_widgets = array(); $last_set_of_blocks = array(); - $post = get_post( $item ); - $blocks = parse_blocks( $post->post_content ); + $blocks = parse_blocks( + $changeset_value ? $changeset_value : get_post( $item )->post_content + ); foreach ( $blocks as $block ) { if ( ! isset( $block['blockName'] ) ) { @@ -379,6 +404,7 @@ public static function swap_out_sidebars_blocks_for_block_widgets( $sidebars_wid $filtered_sidebar_widgets[ $sidebar_id ] = $filtered_widgets; } $sidebars_widgets = $filtered_sidebar_widgets; + return $filtered_sidebar_widgets; } } diff --git a/lib/class-wp-customize-widget-blocks-control.php b/lib/class-wp-customize-widget-blocks-control.php new file mode 100644 index 0000000000000..e92fef67c1577 --- /dev/null +++ b/lib/class-wp-customize-widget-blocks-control.php @@ -0,0 +1,40 @@ + + link(); ?> + /> + add_setting( + 'gutenberg_widget_blocks', + array( + 'default' => '{}', + 'type' => 'gutenberg_widget_blocks', + 'capability' => 'edit_theme_options', + 'transport' => 'postMessage', + 'sanitize_callback' => 'gutenberg_customize_sanitize', + ) + ); + $wp_customize->add_section( + 'gutenberg_widget_blocks', + array( 'title' => __( 'Widget Blocks (Experimental)', 'gutenberg' ) ) + ); + $wp_customize->add_control( + new WP_Customize_Widget_Blocks_Control( + $wp_customize, + 'gutenberg_widget_blocks', + array( + 'section' => 'gutenberg_widget_blocks', + 'settings' => 'gutenberg_widget_blocks', + ) + ) + ); +} +add_action( 'customize_register', 'gutenberg_customize_register' ); + +/** + * Specifies how to save the `gutenberg_widget_blocks` setting. It parses the JSON string and updates the + * referenced widget areas with the new content. + * + * @param string $value The value that is being published. + * @param \WP_Customize_Setting $setting The setting instance. + */ +function gutenberg_customize_update( $value, $setting ) { + foreach ( json_decode( $value ) as $sidebar_id => $sidebar_content ) { + $id_referenced_in_sidebar = Experimental_WP_Widget_Blocks_Manager::get_post_id_referenced_in_sidebar( $sidebar_id ); + + $post_id = wp_insert_post( + array( + 'ID' => $id_referenced_in_sidebar, + 'post_content' => $sidebar_content, + 'post_type' => 'wp_area', + ) + ); + + if ( 0 === $id_referenced_in_sidebar ) { + Experimental_WP_Widget_Blocks_Manager::reference_post_id_in_sidebar( $sidebar_id, $post_id ); + } + } +} +add_action( 'customize_update_gutenberg_widget_blocks', 'gutenberg_customize_update', 10, 2 ); + +/** + * Filters the Customizer widget settings arguments. + * This is needed because the Customizer registers settings for the raw registered widgets, without going through the `sidebars_widgets` filter. + * The `WP_Customize_Widgets` class expects sidebars to have an array of widgets registered, not a post ID. + * This results in the value passed to `sanitize_js_callback` being `null` and throwing an error. + * + * TODO: Figure out why core is not running the `sidebars_widgets` filter for the relevant part of the code. + * Then, either fix it or change this filter to parse the post IDs and then pass them to the original `sanitize_js_callback`. + * + * @param array $args Array of Customizer setting arguments. + * @param string $id Widget setting ID. + * @return array Maybe modified array of Customizer setting arguments. + */ +function filter_widget_customizer_setting_args( $args, $id = null ) { + // Posts won't have a settings ID like widgets. We can use that to remove the sanitization callback. + if ( ! isset( $id ) ) { + unset( $args['sanitize_js_callback'] ); + } + + return $args; +} +add_filter( 'widget_customizer_setting_args', 'filter_widget_customizer_setting_args' ); diff --git a/lib/load.php b/lib/load.php index d93b9dc1f9f0e..7a3c97fb65553 100644 --- a/lib/load.php +++ b/lib/load.php @@ -34,3 +34,4 @@ require dirname( __FILE__ ) . '/demo.php'; require dirname( __FILE__ ) . '/widgets.php'; require dirname( __FILE__ ) . '/widgets-page.php'; +require dirname( __FILE__ ) . '/customizer.php'; diff --git a/lib/widgets-page.php b/lib/widgets-page.php index f790e60d24b7a..27ebadcb9b9aa 100644 --- a/lib/widgets-page.php +++ b/lib/widgets-page.php @@ -9,10 +9,21 @@ * The main entry point for the Gutenberg widgets page. * * @since 5.2.0 + * + * @param string $page The page name the function is being called for, `'gutenberg_customizer'` for the Customizer. */ -function the_gutenberg_widgets() { +function the_gutenberg_widgets( $page = 'gutenberg_page_gutenberg-widgets' ) { ?> -
+
+
+ +
+ + + ); +} + +export default navigateRegions( CustomizerEditWidgetsInitializer ); diff --git a/packages/edit-widgets/src/components/customizer-edit-widgets-initializer/style.scss b/packages/edit-widgets/src/components/customizer-edit-widgets-initializer/style.scss new file mode 100644 index 0000000000000..f7ac0e338ce99 --- /dev/null +++ b/packages/edit-widgets/src/components/customizer-edit-widgets-initializer/style.scss @@ -0,0 +1,18 @@ +.edit-widgets-customizer-edit-widgets-initializer__content { + background: #f1f1f1; + margin: 0; + min-height: 100%; + padding: 30px 0; + + .block-editor-block-list__layout { + padding: 0 0 0 18px; + } + + .block-editor-block-list__empty-block-inserter { + left: -18px; + } + + .block-editor-rich-text__editable[data-is-placeholder-visible="true"] + .block-editor-rich-text__editable.wp-block-paragraph { + padding: 0; + } +} diff --git a/packages/edit-widgets/src/components/customizer-edit-widgets-initializer/sync-customizer.js b/packages/edit-widgets/src/components/customizer-edit-widgets-initializer/sync-customizer.js new file mode 100644 index 0000000000000..724c3e5ea463d --- /dev/null +++ b/packages/edit-widgets/src/components/customizer-edit-widgets-initializer/sync-customizer.js @@ -0,0 +1,126 @@ +/** + * External dependencies + */ +import { throttle } from 'lodash'; + +/** + * WordPress dependencies + */ +import { parse, serialize } from '@wordpress/blocks'; + +/* +Widget area edits made in the Customizer are synced to Customizer +changesets as an object, encoded as a JSON string, where the keys +are widget area IDs and the values are serialized block content. +This file takes care of that syncing using the 2-way data binding +supported by `WP_Customize_Control`s. The process is as follows: + +- On load, the client checks if the current changeset has +widget areas that it can parse and use to hydrate the store. +It will load all widget areas for the current theme, but if +the changeset has content for a given area, it will replace +its actual published content with the changeset's. + +- On edit, the client updates the 2-way bound input with a new object that maps +widget area IDs and the values are serialized block content, encoded +as a JSON string. + +- On publish, a PHP action will parse the JSON string in the +changeset and update all the widget areas in it, to store the +new content. +*/ + +// Get widget areas from the store in an `id => blocks` mapping. +const getWidgetAreasObject = () => { + const { getWidgetAreas, getBlocksFromWidgetArea } = window.wp.data.select( + 'core/edit-widgets' + ); + + return getWidgetAreas().reduce( ( widgetAreasObject, { id } ) => { + widgetAreasObject[ id ] = getBlocksFromWidgetArea( id ); + return widgetAreasObject; + }, {} ); +}; + +// Serialize the provided blocks and render them in the widget area with the provided ID. +const previewBlocksInWidgetArea = throttle( ( id, blocks ) => { + const customizePreviewIframe = document.querySelector( + '#customize-preview > iframe' + ); + if ( ! customizePreviewIframe || ! customizePreviewIframe.contentDocument ) { + return; + } + + const widgetArea = customizePreviewIframe.contentDocument.querySelector( + `[data-customize-partial-placement-context*='"sidebar_id":"${ id }"']` + ); + if ( widgetArea ) { + widgetArea.innerHTML = serialize( blocks ); + widgetArea.parentElement.innerHTML = widgetArea.outerHTML; + } +}, 1000 ); + +// Update the hidden input that has 2-way data binding with Customizer settings. +const updateSettingInputValue = throttle( ( nextWidgetAreas ) => { + const settingInput = document.getElementById( + '_customize-input-gutenberg_widget_blocks' + ); + if ( settingInput ) { + settingInput.value = JSON.stringify( + Object.keys( nextWidgetAreas ).reduce( ( value, id ) => { + value[ id ] = serialize( nextWidgetAreas[ id ] ); + return value; + }, {} ) + ); + settingInput.dispatchEvent( new window.Event( 'change' ) ); + } +}, 1000 ); + +// Check that all the necessary globals are present. +if ( window.wp && window.wp.customize && window.wp.data ) { + // Wait for the Customizer to finish bootstrapping. + window.wp.customize.bind( 'ready', () => + window.wp.customize.previewer.bind( 'ready', () => { + // Try to parse a previous changeset from the hidden input. + let widgetAreas; + try { + widgetAreas = JSON.parse( + document.getElementById( '_customize-input-gutenberg_widget_blocks' ) + .value + ); + widgetAreas = Object.keys( widgetAreas ).reduce( ( value, id ) => { + value[ id ] = parse( widgetAreas[ id ] ); + return value; + }, {} ); + } catch ( err ) { + widgetAreas = {}; + } + + // Wait for setup to finish before overwriting sidebars with changeset data, + // if any, and subscribe to registry changes after that so that we can preview + // changes and update the hidden input's value when any of the widget areas change. + const { setupWidgetAreas, updateBlocksInWidgetArea } = window.wp.data + .dispatch( 'core/edit-widgets' ); + setupWidgetAreas().then( () => { + Object.keys( widgetAreas ).forEach( ( id ) => updateBlocksInWidgetArea( id, widgetAreas[ id ] ) ); + widgetAreas = getWidgetAreasObject(); + window.wp.data.subscribe( () => { + const nextWidgetAreas = getWidgetAreasObject(); + + let didUpdate = false; + for ( const id of Object.keys( nextWidgetAreas ) ) { + if ( widgetAreas[ id ] !== nextWidgetAreas[ id ] ) { + previewBlocksInWidgetArea( id, nextWidgetAreas[ id ] ); + didUpdate = true; + } + } + + if ( didUpdate ) { + updateSettingInputValue( nextWidgetAreas ); + } + widgetAreas = nextWidgetAreas; + } ); + } ); + } ) + ); +} diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js index 8b4bb8573262d..16035513da954 100644 --- a/packages/edit-widgets/src/index.js +++ b/packages/edit-widgets/src/index.js @@ -11,12 +11,13 @@ import { registerCoreBlocks } from '@wordpress/block-library'; import './hooks'; import './store'; import EditWidgetsInitializer from './components/edit-widgets-initializer'; +import CustomizerEditWidgetsInitializer from './components/customizer-edit-widgets-initializer'; /** - * Initilizes the widgets screen + * Initializes the block editor in the widgets screen. * - * @param {string} id Id of the root element to render the screen. - * @param {Object} settings Id of the root element to render the screen. + * @param {string} id ID of the root element to render the screen in. + * @param {Object} settings Block editor settings. */ export function initialize( id, settings ) { registerCoreBlocks(); @@ -27,3 +28,19 @@ export function initialize( id, settings ) { document.getElementById( id ) ); } + +/** + * Initializes the block editor in the widgets Customizer section. + * + * @param {string} id ID of the root element to render the section in. + * @param {Object} settings Block editor settings. + */ +export function customizerInitialize( id, settings ) { + registerCoreBlocks(); + render( + , + document.getElementById( id ) + ); +} diff --git a/packages/edit-widgets/src/style.scss b/packages/edit-widgets/src/style.scss index ab68ef801af1d..aac1067bcfc15 100644 --- a/packages/edit-widgets/src/style.scss +++ b/packages/edit-widgets/src/style.scss @@ -1,3 +1,4 @@ +@import "./components/customizer-edit-widgets-initializer/style.scss"; @import "./components/header/style.scss"; @import "./components/layout/style.scss"; @import "./components/notices/style.scss"; @@ -41,6 +42,11 @@ body.gutenberg_page_gutenberg-widgets { > .components-navigate-regions { height: 100%; } + + &.is-in-customizer { + min-height: initial; + position: initial; + } } /**