diff --git a/lib/blocks.php b/lib/blocks.php index ae8a816b3979ef..1dc7277d85ee80 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -52,6 +52,7 @@ function gutenberg_reregister_core_block_types() { 'search.php' => 'core/search', 'social-link.php' => gutenberg_get_registered_social_link_blocks(), 'tag-cloud.php' => 'core/tag-cloud', + 'site-title.php' => 'core/site-title', ); $registry = WP_Block_Type_Registry::get_instance(); diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 7b7e0aedd55301..c4e7c3a02aa356 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -31,6 +31,7 @@ $z-layers: ( ".wp-block-cover__inner-container": 1, // InnerBlocks area inside cover image block ".wp-block-cover.has-background-dim::before": 1, // Overlay area inside block cover need to be higher than the video background. ".wp-block-cover__video-background": 0, // Video background inside cover block. + ".wp-block-site-title__save-button": 1, // Active pill button ".components-button.is-button {:focus or .is-primary}": 1, diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 86ffa7887d2daf..ebd908a3ed1b0a 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -396,6 +396,7 @@ The default editor settings **experimentalEnableLegacyWidgetBlock boolean Whether the user has enabled the Legacy Widget Block **experimentalEnableMenuBlock boolean Whether the user has enabled the Menu Block **experimentalBlockDirectory boolean Whether the user has enabled the Block Directory + \_\_experimentalEnableFullSiteEditing boolean Whether the user has enabled Full Site Editing # **SkipToSelectedBlock** diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index abc68fa5bf411f..a983abacc36b8a 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -32,6 +32,7 @@ export const PREFERENCES_DEFAULTS = { * __experimentalEnableLegacyWidgetBlock boolean Whether the user has enabled the Legacy Widget Block * __experimentalEnableMenuBlock boolean Whether the user has enabled the Menu Block * __experimentalBlockDirectory boolean Whether the user has enabled the Block Directory + * __experimentalEnableFullSiteEditing boolean Whether the user has enabled Full Site Editing */ export const SETTINGS_DEFAULTS = { alignWide: false, @@ -152,6 +153,7 @@ export const SETTINGS_DEFAULTS = { __experimentalEnableLegacyWidgetBlock: false, __experimentalEnableMenuBlock: false, __experimentalBlockDirectory: false, + __experimentalEnableFullSiteEditing: false, gradients: [ { name: __( 'Vivid cyan blue to vivid purple' ), @@ -228,4 +230,3 @@ export const SETTINGS_DEFAULTS = { }, ], }; - diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index bc75754474b294..59569f4105ba2c 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -36,6 +36,7 @@ @import "./text-columns/editor.scss"; @import "./verse/editor.scss"; @import "./video/editor.scss"; +@import "./site-title/editor.scss"; /** * Import styles from internal editor components used by the blocks. diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index e49f68553b0a6f..3147ebcc0e9cdc 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -62,6 +62,9 @@ import * as classic from './classic'; import * as socialLinks from './social-links'; import * as socialLink from './social-link'; +// Full Site Editing Blocks +import * as siteTitle from './site-title'; + /** * Function to register an individual block. * @@ -162,14 +165,24 @@ export const registerCoreBlocks = () => { * __experimentalRegisterExperimentalCoreBlocks( settings ); * ``` */ -export const __experimentalRegisterExperimentalCoreBlocks = process.env.GUTENBERG_PHASE === 2 ? ( settings ) => { - const { __experimentalEnableLegacyWidgetBlock, __experimentalEnableMenuBlock } = settings; +export const __experimentalRegisterExperimentalCoreBlocks = + process.env.GUTENBERG_PHASE === 2 ? + ( settings ) => { + const { + __experimentalEnableLegacyWidgetBlock, + __experimentalEnableMenuBlock, + __experimentalEnableFullSiteEditing, + } = settings - [ - __experimentalEnableLegacyWidgetBlock ? legacyWidget : null, - __experimentalEnableMenuBlock ? navigationMenu : null, - __experimentalEnableMenuBlock ? navigationMenuItem : null, - socialLinks, - ...socialLink.sites, - ].forEach( registerBlock ); -} : undefined; + ;[ + __experimentalEnableLegacyWidgetBlock ? legacyWidget : null, + __experimentalEnableMenuBlock ? navigationMenu : null, + __experimentalEnableMenuBlock ? navigationMenuItem : null, + socialLinks, + ...socialLink.sites, + + // Register Full Site Editing Blocks. + ...( __experimentalEnableFullSiteEditing ? [ siteTitle ] : [] ), + ].forEach( registerBlock ); + } : + undefined; diff --git a/packages/block-library/src/site-title/block.json b/packages/block-library/src/site-title/block.json new file mode 100644 index 00000000000000..a853cde2517b51 --- /dev/null +++ b/packages/block-library/src/site-title/block.json @@ -0,0 +1,4 @@ +{ + "name": "core/site-title", + "category": "layout" +} diff --git a/packages/block-library/src/site-title/edit.js b/packages/block-library/src/site-title/edit.js new file mode 100644 index 00000000000000..0294fb82d17a18 --- /dev/null +++ b/packages/block-library/src/site-title/edit.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { + useEntityProp, + __experimentalUseEntitySaving, +} from '@wordpress/core-data'; +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { RichText } from '@wordpress/block-editor'; + +export default function SiteTitleEdit() { + const [ title, setTitle ] = useEntityProp( 'root', 'site', 'title' ); + const [ isDirty, isSaving, save ] = __experimentalUseEntitySaving( + 'root', + 'site', + 'title' + ); + return ( + <> + + + + ); +} diff --git a/packages/block-library/src/site-title/editor.scss b/packages/block-library/src/site-title/editor.scss new file mode 100644 index 00000000000000..f2b8359cf3790b --- /dev/null +++ b/packages/block-library/src/site-title/editor.scss @@ -0,0 +1,6 @@ +.wp-block-site-title__save-button { + position: absolute; + right: 0; + top: 0; + z-index: z-index(".wp-block-site-title__save-button"); +} diff --git a/packages/block-library/src/site-title/icon.js b/packages/block-library/src/site-title/icon.js new file mode 100644 index 00000000000000..1ab74dec2c32d3 --- /dev/null +++ b/packages/block-library/src/site-title/icon.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path, Circle } from '@wordpress/components'; + +export default ( + + + + + +); diff --git a/packages/block-library/src/site-title/index.js b/packages/block-library/src/site-title/index.js new file mode 100644 index 00000000000000..4b8fc50b86b34f --- /dev/null +++ b/packages/block-library/src/site-title/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import icon from './icon'; +import edit from './edit'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: __( 'Site Title' ), + icon, + edit, +}; diff --git a/packages/block-library/src/site-title/index.php b/packages/block-library/src/site-title/index.php new file mode 100644 index 00000000000000..6e5b5786436f09 --- /dev/null +++ b/packages/block-library/src/site-title/index.php @@ -0,0 +1,28 @@ +%s', get_bloginfo( 'name' ) ); +} + +/** + * Registers the `core/site-title` block on the server. + */ +function register_block_core_site_title() { + register_block_type( + 'core/site-title', + array( + 'render_callback' => 'render_block_core_site_title', + ) + ); +} +add_action( 'init', 'register_block_core_site_title' ); diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index 4ee2f6b715374f..b75d0e44632373 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -12,6 +12,7 @@ import { apiFetch, select } from './controls'; export const DEFAULT_ENTITY_KEY = 'id'; export const defaultEntities = [ + { name: 'site', kind: 'root', baseURL: '/wp/v2/settings' }, { name: 'postType', kind: 'root', key: 'slug', baseURL: '/wp/v2/types' }, { name: 'media', kind: 'root', baseURL: '/wp/v2/media', plural: 'mediaItems' }, { name: 'taxonomy', kind: 'root', key: 'slug', baseURL: '/wp/v2/taxonomies', plural: 'taxonomies' }, diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index 7870e0217cc974..50439f1b57ad65 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -52,6 +52,17 @@ export default function EntityProvider( { kind, type, id, children } ) { return { children }; } +/** + * Hook that returns the ID for the nearest + * provided entity of the specified type. + * + * @param {string} kind The entity kind. + * @param {string} type The entity type. + */ +export function useEntityId( kind, type ) { + return useContext( getEntity( kind, type ).context ); +} + /** * Hook that returns the value and a setter for the * specified property of the nearest provided @@ -66,11 +77,13 @@ export default function EntityProvider( { kind, type, id, children } ) { * setter. */ export function useEntityProp( kind, type, prop ) { - const id = useContext( getEntity( kind, type ).context ); + const id = useEntityId( kind, type ); const value = useSelect( ( select ) => { - const entity = select( 'core' ).getEditedEntityRecord( kind, type, id ); + const { getEntityRecord, getEditedEntityRecord } = select( 'core' ); + getEntityRecord( kind, type, id ); // Trigger resolver. + const entity = getEditedEntityRecord( kind, type, id ); return entity && entity[ prop ]; }, [ kind, type, id, prop ] @@ -88,3 +101,62 @@ export function useEntityProp( kind, type, prop ) { return [ value, setValue ]; } + +/** + * Hook that returns whether the nearest provided + * entity of the specified type is dirty, saving, + * and a function to save it. + * + * The last, optional parameter is for scoping the + * selection to a single property or a list properties. + * + * By default, dirtyness detection and saving considers + * and handles all properties of an entity, but this + * last parameter lets you scope it to a single property + * or a list of properties for each instance of this hook. + * + * @param {string} kind The entity kind. + * @param {string} type The entity type. + * @param {string|[string]} [props] The property name or list of property names. + */ +export function __experimentalUseEntitySaving( kind, type, props ) { + const id = useEntityId( kind, type ); + + const [ isDirty, isSaving, edits ] = useSelect( + ( select ) => { + const { getEntityRecordNonTransientEdits, isSavingEntityRecord } = select( + 'core' + ); + const _edits = getEntityRecordNonTransientEdits( kind, type, id ); + const editKeys = Object.keys( _edits ); + return [ + props ? + editKeys.some( ( key ) => + typeof props === 'string' ? key === props : props.includes( key ) + ) : + editKeys.length > 0, + isSavingEntityRecord( kind, type, id ), + _edits, + ]; + }, + [ kind, type, id, props ] + ); + + const { saveEntityRecord } = useDispatch( 'core' ); + const save = useCallback( () => { + let filteredEdits = edits; + if ( typeof props === 'string' ) { + filteredEdits = { [ props ]: filteredEdits[ props ] }; + } else if ( props ) { + filteredEdits = filteredEdits.reduce( ( acc, key ) => { + if ( props.includes( key ) ) { + acc[ key ] = filteredEdits[ key ]; + } + return acc; + }, {} ); + } + saveEntityRecord( kind, type, { id, ...filteredEdits } ); + }, [ kind, type, id, props, edits ] ); + + return [ isDirty, isSaving, save ]; +} diff --git a/packages/core-data/src/index.js b/packages/core-data/src/index.js index 2cdddb960e448b..bf12ac2e5096fb 100644 --- a/packages/core-data/src/index.js +++ b/packages/core-data/src/index.js @@ -49,4 +49,9 @@ registerStore( REDUCER_KEY, { resolvers: { ...resolvers, ...entityResolvers }, } ); -export { default as EntityProvider, useEntityProp } from './entity-provider'; +export { + default as EntityProvider, + useEntityId, + useEntityProp, + __experimentalUseEntitySaving, +} from './entity-provider'; diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 8edfdbf895cded..670cb4adf2a750 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -47,7 +47,7 @@ export function* getCurrentUser() { * @param {string} name Entity name. * @param {number} key Record's key */ -export function* getEntityRecord( kind, name, key ) { +export function* getEntityRecord( kind, name, key = '' ) { const entities = yield getKindEntities( kind ); const entity = find( entities, { kind, name } ); if ( ! entity ) { diff --git a/packages/e2e-tests/fixtures/block-transforms.js b/packages/e2e-tests/fixtures/block-transforms.js index ff297c84a5d3da..d14783aa3657fc 100644 --- a/packages/e2e-tests/fixtures/block-transforms.js +++ b/packages/e2e-tests/fixtures/block-transforms.js @@ -425,6 +425,12 @@ export const EXPECTED_TRANSFORMS = { 'Group', ], }, + 'core__site-title': { + availableTransforms: [ + 'Group', + ], + originalBlock: 'Site Title', + }, 'core__social-link-amazon': { availableTransforms: [ 'Group', diff --git a/packages/e2e-tests/fixtures/blocks/core__site-title.html b/packages/e2e-tests/fixtures/blocks/core__site-title.html new file mode 100644 index 00000000000000..3c42ebc0d2feb7 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__site-title.html @@ -0,0 +1 @@ + diff --git a/packages/e2e-tests/fixtures/blocks/core__site-title.json b/packages/e2e-tests/fixtures/blocks/core__site-title.json new file mode 100644 index 00000000000000..6070316cf41796 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__site-title.json @@ -0,0 +1,10 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/site-title", + "isValid": true, + "attributes": {}, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__site-title.parsed.json b/packages/e2e-tests/fixtures/blocks/core__site-title.parsed.json new file mode 100644 index 00000000000000..bc570f8255a124 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__site-title.parsed.json @@ -0,0 +1,18 @@ +[ + { + "blockName": "core/site-title", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__site-title.serialized.html b/packages/e2e-tests/fixtures/blocks/core__site-title.serialized.html new file mode 100644 index 00000000000000..3c42ebc0d2feb7 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__site-title.serialized.html @@ -0,0 +1 @@ + diff --git a/packages/e2e-tests/specs/experimental/block-transforms.test.js b/packages/e2e-tests/specs/experimental/block-transforms.test.js index 16cc6890581dd6..b298fa42f53ef6 100644 --- a/packages/e2e-tests/specs/experimental/block-transforms.test.js +++ b/packages/e2e-tests/specs/experimental/block-transforms.test.js @@ -100,7 +100,11 @@ describe( 'Block transforms', () => { const transformStructure = {}; beforeAll( async () => { - await enableExperimentalFeatures( [ '#gutenberg-widget-experiments', '#gutenberg-menu-block' ] ); + await enableExperimentalFeatures( [ + '#gutenberg-widget-experiments', + '#gutenberg-menu-block', + '#gutenberg-full-site-editing', + ] ); await createNewPost(); for ( const fileBase of fileBasenames ) { diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index c386f02097167b..b57213e0628403 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -109,6 +109,7 @@ class EditorProvider extends Component { '__experimentalEnableLegacyWidgetBlock', '__experimentalEnableMenuBlock', '__experimentalBlockDirectory', + '__experimentalEnableFullSiteEditing', 'showInserterHelpPanel', ] ), __experimentalReusableBlocks: reusableBlocks, @@ -187,19 +188,23 @@ class EditorProvider extends Component { ); return ( - - - { children } - - - { editorSettings.__experimentalBlockDirectory && } - + + + + { children } + + + { editorSettings.__experimentalBlockDirectory && ( + + ) } + + ); } diff --git a/test/integration/full-content/full-content.test.js b/test/integration/full-content/full-content.test.js index 1a50c84a834503..cab70d134b3720 100644 --- a/test/integration/full-content/full-content.test.js +++ b/test/integration/full-content/full-content.test.js @@ -49,7 +49,11 @@ function normalizeParsedBlocks( blocks ) { describe( 'full post content fixture', () => { beforeAll( () => { unstable__bootstrapServerSideBlockDefinitions( require( './server-registered.json' ) ); - const settings = { __experimentalEnableLegacyWidgetBlock: true, __experimentalEnableMenuBlock: true }; + const settings = { + __experimentalEnableLegacyWidgetBlock: true, + __experimentalEnableMenuBlock: true, + __experimentalEnableFullSiteEditing: true, + }; // Load all hooks that modify blocks require( '../../../packages/editor/src/hooks' ); registerCoreBlocks();