diff --git a/docs/manifest.json b/docs/manifest.json index b28dff5a8988b..e571ec2b03ea8 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1481,6 +1481,12 @@ "markdown_source": "../packages/custom-templated-path-webpack-plugin/README.md", "parent": "packages" }, + { + "title": "@wordpress/customize-widgets", + "slug": "packages-customize-widgets", + "markdown_source": "../packages/customize-widgets/README.md", + "parent": "packages" + }, { "title": "@wordpress/data-controls", "slug": "packages-data-controls", diff --git a/lib/class-wp-sidebar-block-editor-control.php b/lib/class-wp-sidebar-block-editor-control.php new file mode 100644 index 0000000000000..cda048ee88191 --- /dev/null +++ b/lib/class-wp-sidebar-block-editor-control.php @@ -0,0 +1,35 @@ +add_data( 'wp-block-directory', 'rtl', 'replace' ); + + gutenberg_override_style( + $styles, + 'wp-customize-widgets', + gutenberg_url( 'build/customize-widgets/style.css' ), + array( 'wp-components', 'wp-block-editor', 'wp-edit-blocks' ), + filemtime( gutenberg_dir_path() . 'build/customize-widgets/style.css' ) + ); + $styles->add_data( 'wp-customize-widgets', 'rtl', 'replace' ); } add_action( 'wp_default_styles', 'gutenberg_register_packages_styles' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index fa35fa81ddfdc..50f7632e5221e 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -51,6 +51,17 @@ function gutenberg_initialize_experiments_settings() { 'id' => 'gutenberg-navigation', ) ); + add_settings_field( + 'gutenberg-widgets-in-customizer', + __( 'Widgets', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Enable Widgets screen in Customizer', 'gutenberg' ), + 'id' => 'gutenberg-widgets-in-customizer', + ) + ); register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/lib/load.php b/lib/load.php index 7772cac90e15e..ffdd3212f40e9 100644 --- a/lib/load.php +++ b/lib/load.php @@ -106,6 +106,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/client-assets.php'; require __DIR__ . '/demo.php'; require __DIR__ . '/widgets.php'; +require __DIR__ . '/widgets-customize.php'; require __DIR__ . '/navigation.php'; require __DIR__ . '/navigation-page.php'; require __DIR__ . '/experiments-page.php'; diff --git a/lib/widgets-customize.php b/lib/widgets-customize.php new file mode 100644 index 0000000000000..ab88b90096cee --- /dev/null +++ b/lib/widgets-customize.php @@ -0,0 +1,63 @@ +sections() as $section ) { + if ( $section instanceof WP_Customize_Sidebar_Section ) { + $section->description = ''; + } + } + foreach ( $manager->controls() as $control ) { + if ( + $control instanceof WP_Widget_Area_Customize_Control || + $control instanceof WP_Widget_Form_Customize_Control + ) { + $manager->remove_control( $control->id ); + } + } + + foreach ( $wp_registered_sidebars as $sidebar_id => $sidebar ) { + $manager->add_setting( + "sidebars_widgets[$sidebar_id]", + array( + 'capability' => 'edit_theme_options', + 'transport' => 'postMessage', + ) + ); + + $manager->add_control( + new WP_Sidebar_Block_Editor_Control( + $manager, + "sidebars_widgets[$sidebar_id]", + array( + 'section' => "sidebar-widgets-$sidebar_id", + 'settings' => "sidebars_widgets[$sidebar_id]", + 'sidebar_id' => $sidebar_id, + ) + ) + ); + } +} + +if ( gutenberg_is_experiment_enabled( 'gutenberg-widgets-in-customizer' ) ) { + add_action( 'customize_register', 'gutenberg_widgets_customize_register' ); +} diff --git a/lib/widgets-page.php b/lib/widgets-page.php index d31b814c72e47..931013502c7ff 100644 --- a/lib/widgets-page.php +++ b/lib/widgets-page.php @@ -1,6 +1,6 @@ + +## Unreleased + +Initial release. diff --git a/packages/customize-widgets/README.md b/packages/customize-widgets/README.md new file mode 100644 index 0000000000000..6ed533cde09b2 --- /dev/null +++ b/packages/customize-widgets/README.md @@ -0,0 +1,17 @@ +# Customize Widgets + +Widgets blocks in Customizer Module for WordPress. + +> This package is meant to be used only with WordPress core. Feel free to use it in your own project but please keep in mind that it might never get fully documented. + +## Installation + +Install the module + +```bash +npm install @wordpress/customize-widgets +``` + +_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._ + +

Code is Poetry.

diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json new file mode 100644 index 0000000000000..52372195f5039 --- /dev/null +++ b/packages/customize-widgets/package.json @@ -0,0 +1,33 @@ +{ + "name": "@wordpress/customize-widgets", + "private": true, + "version": "1.0.0-prerelease", + "description": "Widgets blocks in Customizer Module for WordPress.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ "wordpress" ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/customize-widgets/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/customize-widgets" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@wordpress/block-editor": "file:../block-editor", + "@wordpress/block-library": "file:../block-library", + "@wordpress/blocks": "file:../blocks", + "@wordpress/components": "file:../components", + "@wordpress/element": "file:../element", + "lodash": "^4.17.19" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/index.js b/packages/customize-widgets/src/components/sidebar-block-editor/index.js new file mode 100644 index 0000000000000..a0b28a72d90cb --- /dev/null +++ b/packages/customize-widgets/src/components/sidebar-block-editor/index.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { + BlockEditorProvider, + BlockList, + BlockSelectionClearer, + ObserveTyping, + WritingFlow, +} from '@wordpress/block-editor'; +import { + DropZoneProvider, + FocusReturnProvider, + SlotFillProvider, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import useSidebarBlockEditor from './use-sidebar-block-editor'; + +export default function SidebarBlockEditor( { sidebar } ) { + const [ blocks, onInput, onChange ] = useSidebarBlockEditor( sidebar ); + + return ( + + + + + + + + + + + + + + + + ); +} diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/sidebar-adapter.js b/packages/customize-widgets/src/components/sidebar-block-editor/sidebar-adapter.js new file mode 100644 index 0000000000000..a7ba8f1513874 --- /dev/null +++ b/packages/customize-widgets/src/components/sidebar-block-editor/sidebar-adapter.js @@ -0,0 +1,189 @@ +/** + * External dependencies + */ +import { difference, isEqual, without } from 'lodash'; + +const { wp } = window; + +function parseWidgetId( widgetId ) { + const matches = widgetId.match( /^(.+)-(\d+)$/ ); + if ( matches ) { + return { + idBase: matches[ 1 ], + number: parseInt( matches[ 2 ], 10 ), + }; + } + + // Likely an old single widget. + return { idBase: widgetId }; +} + +function widgetIdToSettingId( widgetId ) { + const { idBase, number } = parseWidgetId( widgetId ); + if ( number ) { + return `widget_${ idBase }[${ number }]`; + } + + return `widget_${ idBase }`; +} + +function parseSettingId( settingId ) { + const matches = settingId.match( /^widget_(.+)(?:\[(\d+)\])$/ ); + if ( matches ) { + return { + idBase: matches[ 1 ], + number: parseInt( matches[ 2 ], 10 ), + }; + } + + return { idBase: settingId }; +} + +function settingIdToWidgetId( settingId ) { + const { idBase, number } = parseSettingId( settingId ); + if ( number ) { + return `${ idBase }-${ number }`; + } + + return idBase; +} + +export default class SidebarAdapter { + constructor( setting, allSettings ) { + this.setting = setting; + this.allSettings = allSettings; + + this.subscribers = new Set(); + + this._handleSettingChange = this._handleSettingChange.bind( this ); + this._handleAllSettingsChange = this._handleAllSettingsChange.bind( + this + ); + } + + subscribe( callback ) { + if ( ! this.subscribers.size ) { + this.setting.bind( this._handleSettingChange ); + this.allSettings.bind( 'change', this._handleAllSettingsChange ); + } + + this.subscribers.add( callback ); + } + + unsubscribe( callback ) { + this.subscribers.delete( callback ); + + if ( ! this.subscribers.size ) { + this.setting.unbind( this._handleSettingChange ); + this.allSettings.unbind( 'change', this._handleAllSettingsChange ); + } + } + + trigger( event ) { + for ( const callback of this.subscribers ) { + callback( event ); + } + } + + _handleSettingChange( newWidgetIds, oldWidgetIds ) { + const addedWidgetIds = difference( newWidgetIds, oldWidgetIds ); + const removedWidgetIds = difference( oldWidgetIds, newWidgetIds ); + + for ( const widgetId of addedWidgetIds ) { + this.trigger( { type: 'widgetAdded', widgetId } ); + } + + for ( const widgetId of addedWidgetIds ) { + this.trigger( { type: 'widgetRemoved', widgetId } ); + } + + if ( ! isEqual( addedWidgetIds, removedWidgetIds ) ) { + this.trigger( { type: 'widgetsReordered', newWidgetIds } ); + } + } + + _handleAllSettingsChange( setting ) { + if ( ! setting.id.startsWith( 'widget_' ) ) { + return; + } + + const widgetId = settingIdToWidgetId( setting.id ); + if ( ! this.setting.get().includes( widgetId ) ) { + return; + } + + this.trigger( { type: 'widgetChanged', widgetId } ); + } + + getWidgetIds() { + return this.setting.get(); + } + + setWidgetIds( widgetIds ) { + this.setting.set( widgetIds ); + } + + getWidget( widgetId ) { + const { idBase, number } = parseWidgetId( widgetId ); + const settingId = widgetIdToSettingId( widgetId ); + const instance = this.allSettings( settingId ).get(); + return { + id: widgetId, + idBase, + number, + instance, + }; + } + + addWidget( widget ) { + const widgetModel = wp.customize.Widgets.availableWidgets.findWhere( { + id_base: widget.idBase, + } ); + + let number = widget.number; + if ( widgetModel.get( 'is_multi' ) && ! number ) { + widgetModel.set( + 'multi_number', + widgetModel.get( 'multi_number' ) + 1 + ); + number = widgetModel.get( 'multi_number' ); + } + + const settingId = number + ? `widget_${ widget.idBase }[${ number }]` + : `widget_${ widget.idBase }`; + + const settingArgs = { + transport: wp.customize.Widgets.data.selectiveRefreshableWidgets[ + widgetModel.get( 'id_base' ) + ] + ? 'postMessage' + : 'refresh', + previewer: this.setting.previewer, + }; + const setting = this.allSettings.create( + settingId, + settingId, + '', + settingArgs + ); + setting.set( {} ); + + const widgetIds = this.setting.get(); + const widgetId = settingIdToWidgetId( settingId ); + this.setting.set( [ ...widgetIds, widgetId ] ); + + return widgetId; + } + + updateWidget( widget ) { + const settingId = widgetIdToSettingId( widget.id ); + this.allSettings( settingId ).set( widget.instance ); + // TODO: what about the other stuff? + } + + removeWidget( widgetId ) { + const widgetIds = this.setting.get(); + this.setting.set( without( widgetIds, widgetId ) ); + } +} diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/use-sidebar-block-editor.js b/packages/customize-widgets/src/components/sidebar-block-editor/use-sidebar-block-editor.js new file mode 100644 index 0000000000000..600bfe67fc3d4 --- /dev/null +++ b/packages/customize-widgets/src/components/sidebar-block-editor/use-sidebar-block-editor.js @@ -0,0 +1,240 @@ +/** + * External dependencies + */ +import { invert, omit, keyBy, isEqual } from 'lodash'; + +/** + * WordPress dependencies + */ +import { serialize, parse, createBlock } from '@wordpress/blocks'; +import { useState, useEffect, useCallback, useRef } from '@wordpress/element'; + +function blockToWidget( block, existingWidget = null ) { + let widget; + + if ( block.name === 'core/legacy-widget' ) { + const isReferenceWidget = !! block.attributes.referenceWidgetName; + if ( isReferenceWidget ) { + widget = { + id: block.attributes.referenceWidgetName, + instance: block.attributes.instance, + }; + } else { + widget = { + widgetClass: block.attributes.widgetClass, + idBase: block.attributes.idBase, + instance: block.attributes.instance, + }; + } + } else { + widget = { + idBase: 'block', + widgetClass: 'WP_Widget_Block', + instance: { content: serialize( block ) }, + }; + } + + return { + ...omit( existingWidget, [ 'form', 'rendered' ] ), + ...widget, + }; +} + +function widgetToBlock( widget, existingBlock = null ) { + let block; + + // FIXME: We'll never get it here with blocks, we need to update this. + if ( widget.widgetClass === 'WP_Widget_Block' ) { + const parsedBlocks = parse( widget.instance.content ); + block = parsedBlocks.length + ? parsedBlocks[ 0 ] + : createBlock( 'core/paragraph', {} ); + } else { + const attributes = { + name: widget.name, + form: widget.form, + instance: widget.instance, + idBase: widget.idBase, + number: widget.number, + }; + + const isReferenceWidget = ! widget.widgetClass; + if ( isReferenceWidget ) { + attributes.referenceWidgetName = widget.id; + } else { + attributes.widgetClass = widget.widgetClass; + } + + block = createBlock( 'core/legacy-widget', attributes, [] ); + } + + return { + ...block, + clientId: existingBlock?.clientId ?? block.clientId, + }; +} + +function initState( sidebar ) { + const state = { blocks: [], widgetIds: {} }; + + for ( const widgetId of sidebar.getWidgetIds() ) { + const widget = sidebar.getWidget( widgetId ); + const block = widgetToBlock( widget ); + state.blocks.push( block ); + state.widgetIds[ block.clientId ] = widgetId; + } + + return state; +} + +export default function useSidebarBlockEditor( sidebar ) { + // TODO: Could/should optimize these data structures so that there's less + // array traversal. In particular, setBlocks() is a really hot path. + + const [ state, setState ] = useState( () => initState( sidebar ) ); + + const ignoreIncoming = useRef( false ); + + useEffect( () => { + const handler = ( event ) => { + if ( ignoreIncoming.current ) { + return; + } + + switch ( event.type ) { + case 'widgetAdded': { + const { widgetId } = event; + const block = blockToWidget( + sidebar.getWidget( widgetId ) + ); + setState( ( lastState ) => ( { + blocks: [ ...lastState.blocks, block ], + widgetIds: { + ...lastState.widgetIds, + [ block.clientId ]: widgetId, + }, + } ) ); + break; + } + + case 'widgetRemoved': { + const { widgetId } = event; + const blockClientId = invert( state.widgetIds )[ widgetId ]; + setState( ( lastState ) => ( { + blocks: lastState.blocks.filter( + ( { clientId } ) => clientId !== blockClientId + ), + widgetIds: omit( lastState.widgetIds, blockClientId ), + } ) ); + break; + } + + case 'widgetChanged': { + const { widgetId } = event; + const blockClientIdToUpdate = invert( state.widgetIds )[ + widgetId + ]; + const blockToUpdate = state.blocks.find( + ( { clientId } ) => clientId === blockClientIdToUpdate + ); + const updatedBlock = widgetToBlock( + sidebar.getWidget( widgetId ), + blockToUpdate + ); + setState( ( lastState ) => ( { + ...lastState, + blocks: lastState.blocks.map( ( block ) => + block.clientId === blockClientIdToUpdate + ? updatedBlock + : block + ), + } ) ); + break; + } + + case 'widgetsReordered': + const { widgetIds } = event; + const blockClientIds = invert( state.widgetIds ); + const blocksByClientId = keyBy( state.blocks, 'clientId' ); + setState( ( lastState ) => ( { + ...lastState, + blocks: widgetIds.map( + ( widgetId ) => + blocksByClientId[ blockClientIds[ widgetId ] ] + ), + } ) ); + break; + } + }; + + sidebar.subscribe( handler ); + return () => sidebar.unsubscribe( handler ); + }, [ sidebar ] ); + + const onChangeBlocks = useCallback( + ( nextBlocks ) => { + ignoreIncoming.current = true; + + let nextWidgetIds = state.widgetIds; + + const blocksByClientId = keyBy( state.blocks, 'clientId' ); + + const seen = {}; + + for ( const nextBlock of nextBlocks ) { + if ( nextBlock.clientId in blocksByClientId ) { + const block = blocksByClientId[ nextBlock.clientId ]; + if ( ! isEqual( block, nextBlock ) ) { + const widgetId = state.widgetIds[ nextBlock.clientId ]; + const widgetToUpdate = sidebar.getWidget( widgetId ); + const widget = blockToWidget( + nextBlock, + widgetToUpdate + ); + sidebar.updateWidget( widget ); + } + } else { + const widget = blockToWidget( nextBlock ); + const widgetId = sidebar.addWidget( widget ); + if ( nextWidgetIds === state.widgetIds ) { + nextWidgetIds = { ...state.widgetIds }; + } + nextWidgetIds[ nextBlock.clientId ] = widgetId; + } + + seen[ nextBlock.clientId ] = true; + } + + for ( const block of state.blocks ) { + if ( ! seen[ block.clientId ] ) { + const widgetId = state.widgetIds[ block.clientId ]; + sidebar.removeWidget( widgetId ); + } + } + + if ( + nextBlocks.length === state.blocks.length && + ! isEqual( + nextBlocks.map( ( { clientId } ) => clientId ), + state.blocks.map( ( { clientId } ) => clientId ) + ) + ) { + const order = nextBlocks.map( + ( { clientId } ) => state.widgetIds[ clientId ] + ); + sidebar.setWidgetIds( order ); + } + + setState( ( lastState ) => ( { + ...lastState, + blocks: nextBlocks, + widgetIds: nextWidgetIds, + } ) ); + + ignoreIncoming.current = false; + }, + [ state, sidebar ] + ); + + return [ state.blocks, onChangeBlocks, onChangeBlocks ]; +} diff --git a/packages/customize-widgets/src/create-sidebar-control.js b/packages/customize-widgets/src/create-sidebar-control.js new file mode 100644 index 0000000000000..464d2524704da --- /dev/null +++ b/packages/customize-widgets/src/create-sidebar-control.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { render } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import SidebarBlockEditor from './components/sidebar-block-editor'; +import SidebarAdapter from './components/sidebar-block-editor/sidebar-adapter'; + +const { wp } = window; + +export default function createSidebarControl() { + return wp.customize.Control.extend( { + ready() { + render( + , + this.container[ 0 ] + ); + }, + } ); +} diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js new file mode 100644 index 0000000000000..47838b0a94da6 --- /dev/null +++ b/packages/customize-widgets/src/index.js @@ -0,0 +1,62 @@ +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; +import { + registerCoreBlocks, + __experimentalGetCoreBlocks, + __experimentalRegisterExperimentalCoreBlocks, +} from '@wordpress/block-library'; + +/** + * Internal dependencies + */ +import createSidebarControl from './create-sidebar-control'; + +const { wp } = window; + +/** + * Initializes the widgets block editor in the customizer. + */ +export function initialize() { + const coreBlocks = __experimentalGetCoreBlocks().filter( + ( block ) => ! [ 'core/more' ].includes( block.name ) + ); + registerCoreBlocks( coreBlocks ); + + if ( process.env.GUTENBERG_PHASE === 2 ) { + __experimentalRegisterExperimentalCoreBlocks(); + } + + // TODO: Register legacy widgets block + registerBlockType( 'core/legacy-widget', { + title: 'Legacy Widget', + attributes: { + widgetClass: { + type: 'string', + }, + referenceWidgetName: { + type: 'string', + }, + name: { + type: 'string', + }, + idBase: { + type: 'string', + }, + number: { + type: 'number', + }, + instance: { + type: 'object', + }, + }, + edit( { attributes } ) { + return
{ JSON.stringify( attributes ) }
; + }, + } ); + + wp.customize.controlConstructor.sidebar_block_editor = createSidebarControl(); +} + +wp.domReady( initialize ); diff --git a/packages/customize-widgets/src/style.scss b/packages/customize-widgets/src/style.scss new file mode 100644 index 0000000000000..70b786d12ed05 --- /dev/null +++ b/packages/customize-widgets/src/style.scss @@ -0,0 +1 @@ +// TODO diff --git a/packages/edit-widgets/src/blocks/legacy-widget/edit/widget-preview.js b/packages/edit-widgets/src/blocks/legacy-widget/edit/widget-preview.js index 7cb6583644a67..6f8bee8cc2ca4 100644 --- a/packages/edit-widgets/src/blocks/legacy-widget/edit/widget-preview.js +++ b/packages/edit-widgets/src/blocks/legacy-widget/edit/widget-preview.js @@ -15,8 +15,10 @@ function WidgetPreview( { widgetAreaId, attributes, hidden, ...props } ) { const HEIGHT_MARGIN = 20; const [ height, setHeight ] = useState( DEFAULT_HEIGHT ); const iframeRef = useRef(); - const currentUrl = document.location.href; - const iframeUrl = addQueryArgs( currentUrl, { + const widgetScreenUrl = addQueryArgs( 'theme.php', { + page: 'gutenberg-widgets', + } ); + const iframeUrl = addQueryArgs( widgetScreenUrl, { 'widget-preview': { ...attributes, sidebarId: widgetAreaId, diff --git a/packages/edit-widgets/src/components/layout/interface.js b/packages/edit-widgets/src/components/layout/interface.js index ad9fed9bfcccf..0f0c36bfe0b15 100644 --- a/packages/edit-widgets/src/components/layout/interface.js +++ b/packages/edit-widgets/src/components/layout/interface.js @@ -45,12 +45,15 @@ function Interface( { blockEditorSettings } ) { ); const { rootClientId, insertionIndex } = useWidgetLibraryInsertionPoint(); - const { hasSidebarEnabled, isInserterOpened } = useSelect( ( select ) => ( { - hasSidebarEnabled: !! select( - interfaceStore - ).getActiveComplementaryArea( editWidgetsStore.name ), - isInserterOpened: !! select( editWidgetsStore ).isInserterOpened(), - } ) ); + const { hasSidebarEnabled, isInserterOpened } = useSelect( + ( select ) => ( { + hasSidebarEnabled: !! select( + interfaceStore + ).getActiveComplementaryArea( editWidgetsStore.name ), + isInserterOpened: !! select( editWidgetsStore ).isInserterOpened(), + } ), + [] + ); const editorStylesRef = useEditorStyles( blockEditorSettings.styles ); // Inserter and Sidebars are mutually exclusive diff --git a/packages/edit-widgets/src/style.scss b/packages/edit-widgets/src/style.scss index 3e3e7ed2e04a7..6b14fdf0faad3 100644 --- a/packages/edit-widgets/src/style.scss +++ b/packages/edit-widgets/src/style.scss @@ -57,5 +57,4 @@ body.widgets-php { display: none; } - @include wordpress-admin-schemes();