diff --git a/lib/blocks.php b/lib/blocks.php index c751656881501..18bee11cad4ed 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -32,6 +32,7 @@ function gutenberg_reregister_core_block_types() { 'tag-cloud.php' => 'core/tag-cloud', 'site-title.php' => 'core/site-title', 'template-part.php' => 'core/template-part', + 'post.php' => 'core/post', 'post-title.php' => 'core/post-title', 'post-content.php' => 'core/post-content', 'post-author.php' => 'core/post-author', diff --git a/lib/compat.php b/lib/compat.php index 6e68f2134b2cf..9d42f3bd7a6b7 100644 --- a/lib/compat.php +++ b/lib/compat.php @@ -125,3 +125,75 @@ function gutenberg_get_post_from_context() { } return get_post(); } + +/** + * Shim that hooks into `pre_render_block` so as to override `render_block` + * with a function that passes `render_callback` the block object as an + * argument and adds support for block context attributes. + * + * @param string $pre_render The pre-rendered content. Defaults to null. + * @param array $block The block being rendered. + * @param array $context The block context. + * + * @return string String of rendered HTML. + */ +function gutenberg_provide_render_callback_with_block_object( $pre_render, $block, $context = array() ) { + global $post; + + $source_block = $block; + /** This filter is documented in src/wp-includes/blocks.php */ + $block = apply_filters( 'render_block_data', $block, $source_block ); + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $is_dynamic = $block['blockName'] && null !== $block_type && $block_type->is_dynamic(); + $block_content = ''; + $index = 0; + + if ( ! is_array( $block['attrs'] ) ) { + $block['attrs'] = array(); + } + + $block['context'] = array(); + $next_context = $context; + if ( $block_type ) { + if ( isset( $block_type->context ) ) { + foreach ( $block_type->context as $attribute_name => $block_names ) { + $block_names = is_array( $block_names ) ? $block_names : array( $block_names ); + foreach ( $block_names as $block_name ) { + if ( isset( $context[ $block_name ][ $attribute_name ] ) ) { + $block['context'][ $attribute_name ] = $context[ $block_name ][ $attribute_name ]; + } + } + } + } + if ( isset( $block_type->attributes ) ) { + foreach ( $block_type->attributes as $attribute_name => $attribute_config ) { + if ( isset( $attribute_config['context'] ) && $attribute_config['context'] && isset( $block['attrs'][ $attribute_name ] ) ) { + if ( ! isset( $next_context[ $block['blockName'] ] ) ) { + $next_context[ $block['blockName'] ] = array(); + } + $next_context[ $block['blockName'] ][ $attribute_name ] = $block['attrs'][ $attribute_name ]; + } + } + } + } + + foreach ( $block['innerContent'] as $chunk ) { + if ( is_string( $chunk ) ) { + $block_content .= $chunk; + } else { + $inner_block = $block['innerBlocks'][ $index++ ]; + $block_content .= gutenberg_provide_render_callback_with_block_object( null, $inner_block, $next_context ); + } + } + + if ( $is_dynamic ) { + $global_post = $post; + + $prepared_attributes = $block_type->prepare_attributes_for_render( $block['attrs'] ); + $block_content = (string) call_user_func( $block_type->render_callback, $prepared_attributes, $block_content, $block ); + $post = $global_post; + } + /** This filter is documented in src/wp-includes/blocks.php */ + return apply_filters( 'render_block', $block_content, $block ); +} +add_filter( 'pre_render_block', 'gutenberg_provide_render_callback_with_block_object', 10, 2 ); diff --git a/packages/block-editor/src/components/block-list/block-context.js b/packages/block-editor/src/components/block-list/block-context.js new file mode 100644 index 0000000000000..ae3e79f2981d6 --- /dev/null +++ b/packages/block-editor/src/components/block-list/block-context.js @@ -0,0 +1,81 @@ +/** + * External dependencies + */ +import { castArray } from 'lodash'; + +/** + * WordPress dependencies + */ +import { createContext, useContext, useMemo } from '@wordpress/element'; + +const context = {}; +const getContext = ( blockName, attributeName ) => { + if ( ! context[ blockName ] ) { + context[ blockName ] = {}; + } + if ( ! context[ blockName ][ attributeName ] ) { + context[ blockName ][ attributeName ] = createContext(); + } + return context[ blockName ][ attributeName ]; +}; + +function Providers( { blockType, blockAttributes, children } ) { + if ( blockType ) { + for ( const [ attributeName, attributeConfig ] of Object.entries( + blockType.attributes + ) ) { + if ( + attributeConfig.context && + blockAttributes[ attributeName ] !== undefined + ) { + const Provider = getContext( blockType.name, attributeName ) + .Provider; + children = ( + + { children } + + ); + } + } + } + + return children; +} + +function BlockContext( { + blockType, + blockAttributes, + Component, + ...componentProps +} ) { + const blockContext = {}; + if ( blockType.context ) { + for ( const [ attributeName, blockNameOrBlockNames ] of Object.entries( + blockType.context + ) ) { + const blockNames = castArray( blockNameOrBlockNames ); + for ( const blockName of blockNames ) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const contextValue = useContext( + getContext( blockName, attributeName ) + ); + if ( contextValue !== undefined ) { + blockContext[ attributeName ] = contextValue; + } + } + } + } + return ( + + blockContext, + Object.values( blockContext ) + ) } + /> + + ); +} +BlockContext.Providers = Providers; +export default BlockContext; diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 448f4b7451456..c928658723f51 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -24,6 +24,7 @@ import { compose, pure, ifCondition } from '@wordpress/compose'; /** * Internal dependencies */ +import _BlockContext from './block-context'; import BlockEdit from '../block-edit'; import BlockInvalidWarning from './block-invalid-warning'; import BlockCrashWarning from './block-crash-warning'; @@ -130,7 +131,10 @@ function BlockListBlock( { // (InspectorControls, etc.) which are inside `BlockEdit` but not // `BlockHTML`, even in HTML mode. let blockEdit = ( - { +BlockList = forwardRef( ( props, ref ) => { const fallbackRef = useRef(); return ( @@ -137,3 +138,5 @@ export default forwardRef( ( props, ref ) => { ); } ); +BlockList.BlockContext = BlockContext; +export default BlockList; diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 6dc247b6d744a..4d952405f3138 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -28,6 +28,7 @@ @import "./navigation-link/editor.scss"; @import "./nextpage/editor.scss"; @import "./paragraph/editor.scss"; +@import "./post/editor.scss"; @import "./post-excerpt/editor.scss"; @import "./pullquote/editor.scss"; @import "./quote/editor.scss"; diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 206c705651e9a..ecd760f7ec792 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -65,6 +65,7 @@ import * as socialLink from './social-link'; // Full Site Editing Blocks import * as siteTitle from './site-title'; import * as templatePart from './template-part'; +import * as post from './post'; import * as postTitle from './post-title'; import * as postContent from './post-content'; import * as postAuthor from './post-author'; @@ -197,6 +198,7 @@ export const __experimentalRegisterExperimentalCoreBlocks = ? [ siteTitle, templatePart, + post, postTitle, postContent, postAuthor, diff --git a/packages/block-library/src/post-content/block.json b/packages/block-library/src/post-content/block.json index 5c598d0970f49..7327e3b1d5b87 100644 --- a/packages/block-library/src/post-content/block.json +++ b/packages/block-library/src/post-content/block.json @@ -1,4 +1,8 @@ { "name": "core/post-content", - "category": "layout" + "category": "layout", + "context": { + "postType": "core/post", + "postId": "core/post" + } } diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js index 3336e0d819967..20e26c43c618e 100644 --- a/packages/block-library/src/post-content/edit.js +++ b/packages/block-library/src/post-content/edit.js @@ -1,9 +1,30 @@ -export default function PostContentEdit() { +/** + * WordPress dependencies + */ +import { useEntityBlockEditor } from '@wordpress/core-data'; +import { InnerBlocks } from '@wordpress/block-editor'; + +function PostContentInnerBlocks( { postType, postId } ) { + const [ blocks, onInput, onChange ] = useEntityBlockEditor( + 'postType', + postType, + { + id: postId, + } + ); return ( -

- { - 'Welcome to WordPress and the wonderful world of blocks. This content represents how a post would look when editing block templates.' - } -

+ ); } + +export default function PostContentEdit( { context: { postType, postId } } ) { + if ( ! postType || ! postId ) { + return 'Post Content Placeholder'; + } + + return ; +} diff --git a/packages/block-library/src/post-content/index.php b/packages/block-library/src/post-content/index.php index 1e6870fd0b313..87d87a2b38215 100644 --- a/packages/block-library/src/post-content/index.php +++ b/packages/block-library/src/post-content/index.php @@ -8,16 +8,20 @@ /** * Renders the `core/post-content` block on the server. * + * @param array $attributes The block attributes. + * @param array $content The saved content. + * @param array $block The parsed block. + * * @return string Returns the filtered post content of the current post. */ -function render_block_core_post_content() { - $post = gutenberg_get_post_from_context(); +function render_block_core_post_content( $attributes, $content, $block ) { + $post = isset( $block['context']['postId'] ) ? $block['context']['postId'] : gutenberg_get_post_from_context(); if ( ! $post ) { return ''; } return ( '
' . - apply_filters( 'the_content', str_replace( ']]>', ']]>', get_the_content( $post ) ) ) . + apply_filters( 'the_content', str_replace( ']]>', ']]>', get_the_content( null, false, $post ) ) ) . '
' ); } @@ -34,6 +38,10 @@ function register_block_core_post_content() { array_merge( $metadata, array( + 'context' => array( + 'postType' => 'core/post', + 'postId' => 'core/post', + ), 'render_callback' => 'render_block_core_post_content', ) ) diff --git a/packages/block-library/src/post-title/block.json b/packages/block-library/src/post-title/block.json index b64dbc3740ab4..b3baececd016f 100644 --- a/packages/block-library/src/post-title/block.json +++ b/packages/block-library/src/post-title/block.json @@ -1,4 +1,8 @@ { "name": "core/post-title", - "category": "layout" + "category": "layout", + "context": { + "postType": "core/post", + "postId": "core/post" + } } diff --git a/packages/block-library/src/post-title/edit.js b/packages/block-library/src/post-title/edit.js index aa31586b212ab..e7a45a39e0ddd 100644 --- a/packages/block-library/src/post-title/edit.js +++ b/packages/block-library/src/post-title/edit.js @@ -1,3 +1,32 @@ -export default function PostTitleEdit() { - return

{ 'Hello world!' }

; +/** + * WordPress dependencies + */ +import { useEntityProp } from '@wordpress/core-data'; +import { RichText } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; + +function PostTitleInput( { postType, postId } ) { + const [ title, setTitle ] = useEntityProp( + 'postType', + postType, + 'title', + postId + ); + return ( + + ); +} + +export default function PostTitleEdit( { context: { postType, postId } } ) { + if ( ! postType || ! postId ) { + return 'Post Title Placeholder'; + } + + return ; } diff --git a/packages/block-library/src/post-title/index.php b/packages/block-library/src/post-title/index.php index c635ad2a0f0eb..2adf997848401 100644 --- a/packages/block-library/src/post-title/index.php +++ b/packages/block-library/src/post-title/index.php @@ -8,10 +8,14 @@ /** * Renders the `core/post-title` block on the server. * + * @param array $attributes The block attributes. + * @param array $content The saved content. + * @param array $block The parsed block. + * * @return string Returns the filtered post title for the current post wrapped inside "h1" tags. */ -function render_block_core_post_title() { - $post = gutenberg_get_post_from_context(); +function render_block_core_post_title( $attributes, $content, $block ) { + $post = isset( $block['context']['postId'] ) ? $block['context']['postId'] : gutenberg_get_post_from_context(); if ( ! $post ) { return ''; } @@ -30,6 +34,10 @@ function register_block_core_post_title() { array_merge( $metadata, array( + 'context' => array( + 'postType' => 'core/post', + 'postId' => 'core/post', + ), 'render_callback' => 'render_block_core_post_title', ) ) diff --git a/packages/block-library/src/post/block.json b/packages/block-library/src/post/block.json new file mode 100644 index 0000000000000..484a729d30729 --- /dev/null +++ b/packages/block-library/src/post/block.json @@ -0,0 +1,14 @@ +{ + "name": "core/post", + "category": "layout", + "attributes": { + "postType": { + "type": "string", + "context": true + }, + "postId": { + "type": "string", + "context": true + } + } +} diff --git a/packages/block-library/src/post/edit/index.js b/packages/block-library/src/post/edit/index.js new file mode 100644 index 0000000000000..e3e132ff54559 --- /dev/null +++ b/packages/block-library/src/post/edit/index.js @@ -0,0 +1,34 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { InnerBlocks } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import PostPlaceholder from './placeholder'; + +export default function PostEdit( { + attributes: { postType, postId }, + setAttributes, +} ) { + const loaded = useSelect( + ( select ) => { + if ( ! postType || ! postId ) { + return false; + } + return Boolean( + select( 'core' ).getEntityRecord( 'postType', postType, postId ) + ); + }, + [ postType, postId ] + ); + if ( postType && postId ) { + if ( ! loaded ) { + return null; + } + return ; + } + return ; +} diff --git a/packages/block-library/src/post/edit/placeholder.js b/packages/block-library/src/post/edit/placeholder.js new file mode 100644 index 0000000000000..bed0b0e1d0161 --- /dev/null +++ b/packages/block-library/src/post/edit/placeholder.js @@ -0,0 +1,81 @@ +/** + * WordPress dependencies + */ +import { useEntityBlockEditor } from '@wordpress/core-data'; +import { __ } from '@wordpress/i18n'; +import { BlockPreview } from '@wordpress/block-editor'; +import { useState, useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { Placeholder, TextControl, Button } from '@wordpress/components'; + +function PostPreview( { postType, postId } ) { + const [ blocks ] = useEntityBlockEditor( 'postType', postType, { + id: postId, + } ); + return ( +
+
+ { __( 'Preview' ) } +
+ +
+ ); +} + +export default function PostPlaceholder( { setAttributes } ) { + const [ postType, setPostType ] = useState( 'post' ); + const [ postId, setPostId ] = useState(); + const preview = useSelect( + ( select ) => { + if ( ! postType || ! postId ) { + return null; + } + const post = select( 'core' ).getEntityRecord( + 'postType', + postType, + postId + ); + if ( post ) { + return ; + } + }, + [ postType, postId ] + ); + const choosePost = useCallback( + () => setAttributes( { postType, postId } ), + [ postType, postId ] + ); + return ( + +
+ + +
+ { preview } + +
+ ); +} diff --git a/packages/block-library/src/post/editor.scss b/packages/block-library/src/post/editor.scss new file mode 100644 index 0000000000000..45f6744e6ef45 --- /dev/null +++ b/packages/block-library/src/post/editor.scss @@ -0,0 +1,28 @@ +.wp-block-post__placeholder-input-container { + display: flex; + flex-wrap: wrap; + width: 100%; +} + +.wp-block-post__placeholder-input { + margin: 5px; +} + +.wp-block-post__placeholder-preview { + margin-bottom: 15px; + width: 100%; + + .block-editor-block-preview__container { + padding: 1px; + } + + .block-editor-block-preview__content { + position: initial; + } +} + +.wp-block-post__placeholder-preview-title { + font-size: 15px; + font-weight: 600; + margin-bottom: 4px; +} diff --git a/packages/block-library/src/post/index.js b/packages/block-library/src/post/index.js new file mode 100644 index 0000000000000..3d7ab2f8393b9 --- /dev/null +++ b/packages/block-library/src/post/index.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit'; +import save from './save'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: __( 'Post' ), + edit, + save, +}; diff --git a/packages/block-library/src/post/index.php b/packages/block-library/src/post/index.php new file mode 100644 index 0000000000000..458d334ed3e82 --- /dev/null +++ b/packages/block-library/src/post/index.php @@ -0,0 +1,19 @@ +; +} diff --git a/packages/core-data/src/entity-provider.js b/packages/core-data/src/entity-provider.js index d2d0f23569ca4..8fb8b4b71aa5b 100644 --- a/packages/core-data/src/entity-provider.js +++ b/packages/core-data/src/entity-provider.js @@ -74,16 +74,25 @@ export function useEntityId( kind, type ) { * specified property of the nearest provided * entity of the specified type. * - * @param {string} kind The entity kind. - * @param {string} type The entity type. - * @param {string} prop The property name. + * You may optionally override the provided ID + * using the last parameter. + * + * @param {string} kind The entity kind. + * @param {string} type The entity type. + * @param {string} prop The property name. + * @param {string} [id=useEntityId( kind, type )] The optional entity ID override. * * @return {[*, Function]} A tuple where the first item is the * property value and the second is the * setter. */ -export function useEntityProp( kind, type, prop ) { - const id = useEntityId( kind, type ); +export function useEntityProp( + kind, + type, + prop, + // eslint-disable-next-line react-hooks/rules-of-hooks + id = useEntityId( kind, type ) +) { const { value, fullValue } = useSelect( ( select ) => { const { getEntityRecord, getEditedEntityRecord } = select( 'core' ); @@ -122,24 +131,38 @@ export function useEntityProp( kind, type, prop ) { * `BlockEditorProvider` and are intended to be used with it, * or similar components or hooks. * - * @param {string} kind The entity kind. - * @param {string} type The entity type. + * You may optionally override the provided ID + * using the last parameter. + * + * @param {string} kind The entity kind. + * @param {string} type The entity type. * @param {Object} options - * @param {Object} [options.initialEdits] Initial edits object for the entity record. - * @param {string} [options.blocksProp='blocks'] The name of the entity prop that holds the blocks array. - * @param {string} [options.contentProp='content'] The name of the entity prop that holds the serialized blocks. + * @param {Object} [options.initialEdits] Initial edits object for the entity record. + * @param {string} [options.blocksProp='blocks'] The name of the entity prop that holds the blocks array. + * @param {string} [options.contentProp='content'] The name of the entity prop that holds the serialized blocks. + * @param {string} [options.id=useEntityId( kind, type )] The optional entity ID override. * * @return {[WPBlock[], Function, Function]} The block array and setters. */ export function useEntityBlockEditor( kind, type, - { initialEdits, blocksProp = 'blocks', contentProp = 'content' } = {} + { + initialEdits, + blocksProp = 'blocks', + contentProp = 'content', + // eslint-disable-next-line react-hooks/rules-of-hooks + id = useEntityId( kind, type ), + } = {} ) { - const [ content, setContent ] = useEntityProp( kind, type, contentProp ); + const [ content, setContent ] = useEntityProp( + kind, + type, + contentProp, + id + ); const { editEntityRecord } = useDispatch( 'core' ); - const id = useEntityId( kind, type ); const initialBlocks = useMemo( () => { if ( initialEdits ) { editEntityRecord( kind, type, id, initialEdits, { @@ -157,7 +180,8 @@ export function useEntityBlockEditor( const [ blocks = initialBlocks, onInput ] = useEntityProp( kind, type, - blocksProp + blocksProp, + id ); const onChange = useCallback( diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 74c7b2f67cf72..016988a64447b 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -15,10 +15,12 @@ import { EntityProvider } from '@wordpress/core-data'; import { BlockEditorProvider, __unstableEditorStyles as EditorStyles, + BlockList, } from '@wordpress/block-editor'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; +import { getBlockType } from '@wordpress/blocks'; /** * Internal dependencies @@ -193,19 +195,27 @@ class EditorProvider extends Component { type={ post.type } id={ post.id } > - - { children } - - - + + { children } + + + +