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();