diff --git a/bin/webpack.blocks.js b/bin/webpack.blocks.js
index f986bbf65..ccddbbfd7 100644
--- a/bin/webpack.blocks.js
+++ b/bin/webpack.blocks.js
@@ -23,6 +23,7 @@ module.exports = [
'react-dom': [ 'cf', 'vendor', 'react-dom' ],
'nanoid': [ 'cf', 'vendor', 'nanoid' ],
'immer': [ 'cf', 'vendor', 'immer' ],
+ '@wordpress/api-fetch': [ 'wp', 'apiFetch' ],
'@wordpress/components': [ 'wp', 'components' ],
'@wordpress/blocks': [ 'wp', 'blocks' ],
'@wordpress/editor': [ 'wp', 'editor' ],
diff --git a/config.php b/config.php
index 69c86edab..c681d73f4 100644
--- a/config.php
+++ b/config.php
@@ -4,7 +4,7 @@
# Define version constant
if ( ! defined( __NAMESPACE__ . '\VERSION' ) ) {
- define( __NAMESPACE__ . '\VERSION', '3.0.2' );
+ define( __NAMESPACE__ . '\VERSION', '3.1.0-beta.1' );
}
# Define root directory
diff --git a/core/Container/Block_Container.php b/core/Container/Block_Container.php
index 5b5e5774c..b202a8bbc 100644
--- a/core/Container/Block_Container.php
+++ b/core/Container/Block_Container.php
@@ -11,6 +11,14 @@ class Block_Container extends Container {
*/
public $settings = array(
'preview' => true,
+ 'parent' => null,
+ 'inner_blocks' => array(
+ 'enabled' => false,
+ 'position' => 'above',
+ 'template' => null,
+ 'template_lock' => null,
+ 'allowed_blocks' => null,
+ ),
'category' => array(
'slug' => 'common',
),
@@ -221,6 +229,117 @@ public function set_preview_mode( $preview = true ) {
return $this;
}
+ /**
+ * Set the parent block(s) in which the block type can be inserted.
+ *
+ * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-registration/#parent-optional
+ *
+ * @param string|string[]|null $parent
+ * @return Block_Container
+ */
+ public function set_parent( $parent = null ) {
+ if ( ! is_array( $parent ) && ! is_string( $parent ) && ! is_null( $parent ) ) {
+ throw new \Exception( __( "The parent must be 'array', 'string' or 'null'.", 'crb' ) );
+ }
+
+ $this->settings[ 'parent' ] = is_string( $parent ) ? array( $parent ) : $parent;
+
+ return $this;
+ }
+
+ /**
+ * Set whether the inner blocks are available for the block type.
+ *
+ * @param boolean $inner_blocks
+ * @return Block_Container
+ */
+ public function set_inner_blocks( $inner_blocks = true ) {
+ $this->settings[ 'inner_blocks' ][ 'enabled' ] = $inner_blocks;
+
+ return $this;
+ }
+
+ /**
+ * Set the position of the inner blocks to be rendered
+ * above or below the fields.
+ *
+ * @param string $position
+ * @return Block_Container
+ */
+ public function set_inner_blocks_position( $position = 'above' ) {
+ if ( ! in_array( $position, [ 'above', 'below' ] ) ) {
+ throw new \Exception( __( "The position of inner blocks must be 'above' or 'below'.", 'crb' ) );
+ }
+
+ $this->settings[ 'inner_blocks' ][ 'position' ] = $position;
+
+ return $this;
+ }
+
+ /**
+ * Set the default template that should be rendered in inner blocks.
+ *
+ * @see https://github.com/WordPress/gutenberg/tree/master/packages/editor/src/components/inner-blocks#template
+ *
+ * @param array[]|null $template
+ * @return Block_Container
+ */
+ public function set_inner_blocks_template( $template = null ) {
+ if ( ! is_array( $template ) && ! is_null( $template ) ) {
+ throw new \Exception( __( "The template must be an 'array' or 'null'.", 'crb' ) );
+ }
+
+ $this->settings[ 'inner_blocks' ][ 'template' ] = $template;
+
+ return $this;
+ }
+
+ /**
+ * Set the lock mode used by template of inner blocks.
+ *
+ * @see https://github.com/WordPress/gutenberg/tree/master/packages/editor/src/components/inner-blocks#templatelock
+ *
+ * @param string|boolean|null $lock
+ * @return Block_Container
+ */
+ public function set_inner_blocks_template_lock( $lock = null ) {
+ if ( is_string( $lock ) && ! in_array( $lock, [ 'all', 'insert' ] ) ) {
+ throw new \Exception( __( "The template lock must be 'all', 'insert', 'false' or 'null'.", 'crb' ) );
+ }
+
+ $this->settings[ 'inner_blocks' ][ 'template_lock' ] = $lock;
+
+ return $this;
+ }
+
+ /**
+ * Set the list of allowed blocks that can be inserted.
+ *
+ * @see https://github.com/WordPress/gutenberg/tree/master/packages/editor/src/components/inner-blocks#allowedblocks
+ *
+ * @param string[]|null $blocks
+ * @return Block_Container
+ */
+ public function set_allowed_inner_blocks( $blocks = null ) {
+ if ( ! is_array( $blocks ) && ! is_null( $blocks ) ) {
+ throw new \Exception( __( "The allowed blocks must be an 'array' or 'null'.", 'crb' ) );
+ }
+
+ if ( is_array( $blocks ) ) {
+ $this->settings[ 'inner_blocks' ][ 'allowed_blocks' ] = array_map( function ( $block ) {
+ if ( $block instanceof self ) {
+ return $block->get_block_type_name();
+ }
+
+ return $block;
+ }, $blocks );
+ } else {
+ $this->settings[ 'inner_blocks' ][ 'allowed_blocks' ] = $blocks;
+ }
+
+ return $this;
+ }
+
/**
* Set the render callback of the block type.
*
@@ -239,16 +358,30 @@ public function set_render_callback( $render_callback ) {
* Render the block type.
*
* @param array $attributes
+ * @param string $content
* @return string
*/
- public function render_block( $attributes ) {
+ public function render_block( $attributes, $content ) {
+ $fields = $attributes['data'];
+
+ // Unset the "data" property because we
+ // pass it as separate argument to the callback.
+ unset($attributes['data']);
+
ob_start();
- call_user_func( $this->render_callback , $attributes[ 'data' ] );
+ call_user_func( $this->render_callback , $fields, $attributes, $content );
return ob_get_clean();
}
+ /**
+ * Returns the block type name, e.g. "carbon-fields/testimonial"
+ */
+ private function get_block_type_name() {
+ return str_replace( 'carbon-fields-container-', 'carbon-fields/', str_replace( '_', '-', $this->id ) );
+ }
+
/**
* Register the block type.
*
@@ -263,7 +396,6 @@ protected function register_block() {
throw new \Exception( __( "'render_callback' must be a callable.", 'crb' ) );
}
- $name = str_replace( 'carbon-fields-container-', 'carbon-fields/', str_replace( '_', '-', $this->id ) );
$style = isset( $this->settings[ 'style' ] ) ? $this->settings[ 'style' ] : null;
$editor_style = isset( $this->settings[ 'editor_style' ] ) ? $this->settings[ 'editor_style' ] : null;
$attributes = array_reduce( $this->get_fields(), function( $attributes, $field ) {
@@ -277,7 +409,7 @@ protected function register_block() {
),
) );
- register_block_type( $name, array(
+ register_block_type( $this->get_block_type_name(), array(
'style' => $style,
'editor_style' => $editor_style,
'attributes' => $attributes,
diff --git a/core/REST_API/Router.php b/core/REST_API/Router.php
index 34c97847a..187aaae85 100644
--- a/core/REST_API/Router.php
+++ b/core/REST_API/Router.php
@@ -58,6 +58,13 @@ class Router {
'permission_callback' => 'allow_access',
'methods' => 'GET',
),
+ 'block_renderer' => array(
+ 'path' => '/block-renderer',
+ 'callback' => 'block_renderer',
+ 'permission_callback' => 'block_renderer_permission',
+ 'methods' => 'POST',
+ 'args' => 'block_renderer_args_schema',
+ )
);
/**
@@ -177,6 +184,7 @@ protected function register_route( $route ) {
'methods' => $route['methods'],
'permission_callback' => array( $this, $route['permission_callback'] ),
'callback' => array( $this, $route['callback'] ),
+ 'args' => isset( $route['args'] ) ? call_user_func( array( $this, $route['args'] ) ) : array(),
) );
}
@@ -351,4 +359,110 @@ protected function set_options( $request ) {
return new \WP_REST_Response( __( 'Theme Options updated.', 'carbon-fields' ), 200 );
}
+
+ /**
+ * Checks if a given request has access to read blocks.
+ *
+ * @see https://github.com/WordPress/WordPress/blob/master/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php#L78-L116
+ *
+ * @param WP_REST_Request
+ * @return true|WP_Error
+ */
+ public function block_renderer_permission( $request ) {
+ global $post;
+
+ $post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0;
+
+ if ( 0 < $post_id ) {
+ $post = get_post( $post_id );
+
+ if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) {
+ return new \WP_Error(
+ 'block_cannot_read',
+ __( 'Sorry, you are not allowed to read blocks of this post.', 'carbon-fields' ),
+ array(
+ 'status' => rest_authorization_required_code(),
+ )
+ );
+ }
+ } else {
+ if ( ! current_user_can( 'edit_posts' ) ) {
+ return new \WP_Error(
+ 'block_cannot_read',
+ __( 'Sorry, you are not allowed to read blocks as this user.', 'carbon-fields' ),
+ array(
+ 'status' => rest_authorization_required_code(),
+ )
+ );
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the schema of the accepted arguments.
+ *
+ * @see https://github.com/WordPress/WordPress/blob/master/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php#L56-L71
+ *
+ * @return array
+ */
+ public function block_renderer_args_schema() {
+ return array(
+ 'name' => array(
+ 'type' => 'string',
+ 'required' => true,
+ 'description' => __( 'The name of the block.', 'carbon-fields' ),
+ ),
+ 'content' => array(
+ 'type' => 'string',
+ 'required' => true,
+ 'description' => __( 'The content of the block.', 'carbon-fields' ),
+ ),
+ 'post_id' => array(
+ 'type' => 'integer',
+ 'description' => __( 'ID of the post context.', 'carbon-fields' ),
+ ),
+ );
+ }
+
+ /**
+ * Returns block output from block's registered render_callback.
+ *
+ * @see https://github.com/WordPress/WordPress/blob/master/wp-includes/rest-api/endpoints/class-wp-rest-block-renderer-controller.php#L118-L154
+ *
+ * @param WP_REST_Request $request
+ * @return WP_REST_Response|WP_Error
+ */
+ public function block_renderer( $request ) {
+ global $post;
+
+ $post_id = isset( $request['post_id'] ) ? intval( $request['post_id'] ) : 0;
+
+ if ( 0 < $post_id ) {
+ $post = get_post( $post_id );
+
+ // Set up postdata since this will be needed if post_id was set.
+ setup_postdata( $post );
+ }
+
+ $registry = \WP_Block_Type_Registry::get_instance();
+ $block = $registry->get_registered( $request['name'] );
+
+ if ( null === $block ) {
+ return new \WP_Error(
+ 'block_invalid',
+ __( 'Invalid block.' ),
+ array(
+ 'status' => 404,
+ )
+ );
+ }
+
+ $data = array(
+ 'rendered' => do_blocks( $request['content'] ),
+ );
+
+ return rest_ensure_response( $data );
+ }
}
diff --git a/package.json b/package.json
index cfc78a439..2b35e43d2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "carbon-fields",
- "version": "3.0.2",
+ "version": "3.1.0-beta.1",
"description": "WordPress developer-friendly custom fields for post types, taxonomy terms, users, comments, widgets, options, navigation menus and more.",
"directories": {
"test": "tests"
@@ -61,6 +61,7 @@
},
"dependencies": {
"@babel/runtime": "^7.1.5",
+ "@wordpress/api-fetch": "^2.2.8",
"@wordpress/blocks": "^6.0.2",
"@wordpress/components": "^7.0.3",
"@wordpress/compose": "^3.0.0",
diff --git a/packages/blocks/components/block-edit/index.js b/packages/blocks/components/block-edit/index.js
index 9a3cb49e4..b9041bf94 100644
--- a/packages/blocks/components/block-edit/index.js
+++ b/packages/blocks/components/block-edit/index.js
@@ -3,12 +3,12 @@
*/
import cx from 'classnames';
import { Component, Fragment } from '@wordpress/element';
+import { Toolbar, PanelBody } from '@wordpress/components';
import {
- Toolbar,
- PanelBody,
- ServerSideRender
-} from '@wordpress/components';
-import { BlockControls, InspectorControls } from '@wordpress/editor';
+ InnerBlocks,
+ BlockControls,
+ InspectorControls
+} from '@wordpress/editor';
import { withSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import {
@@ -27,6 +27,7 @@ import { getFieldType } from '@carbon-fields/core';
*/
import './style.scss';
import Field from '../field';
+import ServerSideRender from '../server-side-render';
class BlockEdit extends Component {
/**
@@ -182,15 +183,27 @@ class BlockEdit extends Component {
const { currentTab } = this.state;
const {
- name,
+ clientId,
container,
- attributes,
supportsTabs,
- supportsPreview
+ supportsPreview,
+ supportsInnerBlocks
} = this.props;
+ const innerBlocks = ( ( supportsInnerBlocks && this.isInEditMode ) && (
+
+
+
+ ) );
+
return (
+ { container.settings.inner_blocks.position === 'above' && innerBlocks }
+
{ supportsPreview && (
-
+
) }
+ { container.settings.inner_blocks.position === 'below' && innerBlocks }
+
{ this.isInPreviewMode && (
{
@@ -280,17 +295,21 @@ class BlockEdit extends Component {
}
}
-export default withSelect( ( select, { name } ) => {
+export default withSelect( ( select, { clientId, name } ) => {
const { hasBlockSupport } = select( 'core/blocks' );
+ const { getBlockRootClientId } = select( 'core/editor' );
const {
getContainerDefinitionByBlockName,
getFieldDefinitionsByBlockName
} = select( 'carbon-fields/blocks' );
+ const rootClientId = getBlockRootClientId( clientId );
+
return {
container: getContainerDefinitionByBlockName( name ),
fields: getFieldDefinitionsByBlockName( name ),
supportsTabs: hasBlockSupport( name, 'tabs' ),
- supportsPreview: hasBlockSupport( name, 'preview' )
+ supportsPreview: hasBlockSupport( name, 'preview' ) && ! rootClientId,
+ supportsInnerBlocks: hasBlockSupport( name, 'innerBlocks' )
};
} )( BlockEdit );
diff --git a/packages/blocks/components/block-edit/style.scss b/packages/blocks/components/block-edit/style.scss
index af25e7270..e60c1a5c8 100644
--- a/packages/blocks/components/block-edit/style.scss
+++ b/packages/blocks/components/block-edit/style.scss
@@ -44,3 +44,8 @@
.cf-block__preview {
min-height: 100px;
}
+
+.cf-block__inner-blocks .block-list-appender {
+ margin-top: 32px;
+ margin-bottom: 32px;
+}
diff --git a/packages/blocks/components/block-save/index.js b/packages/blocks/components/block-save/index.js
index c3a80144d..12138e02e 100644
--- a/packages/blocks/components/block-save/index.js
+++ b/packages/blocks/components/block-save/index.js
@@ -2,16 +2,40 @@
* External dependencies.
*/
import { Component } from '@wordpress/element';
+import { InnerBlocks } from '@wordpress/editor';
class BlockSave extends Component {
/**
* Render the component.
*
- * @return {Object}
+ * @return {null}
*/
render() {
return null;
}
}
+/**
+ * Adds the content of inner blocks to the saved content.
+ *
+ * @param {mixed} element
+ * @param {Object} blockType
+ * @return {mixed}
+ */
+function addInnerBlocksContent( element, blockType ) {
+ if ( ! /^carbon\-fields\/.+$/.test( blockType.name ) ) {
+ return element;
+ }
+
+ if ( ! blockType.supports.innerBlocks ) {
+ return element;
+ }
+
+ return (
+
+ );
+}
+
+wp.hooks.addFilter( 'blocks.getSaveElement', 'carbon-fields/blocks', addInnerBlocksContent );
+
export default BlockSave;
diff --git a/packages/blocks/components/server-side-render/index.js b/packages/blocks/components/server-side-render/index.js
new file mode 100644
index 000000000..2320ec2c6
--- /dev/null
+++ b/packages/blocks/components/server-side-render/index.js
@@ -0,0 +1,154 @@
+/**
+ * External dependencies.
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { Component, RawHTML } from '@wordpress/element';
+import { Placeholder, Spinner } from '@wordpress/components';
+import { withSelect } from '@wordpress/data';
+import { serialize } from '@wordpress/blocks';
+import { __, sprintf } from '@wordpress/i18n';
+import { isEqual, debounce } from 'lodash';
+
+/**
+ * This component is slightly modified version of the `ServerSideRender` component
+ * that comes by default with Gutenberg.
+ *
+ * @see https://github.com/WordPress/gutenberg/tree/master/packages/components/src/server-side-render
+ */
+class ServerSideRender extends Component {
+ /**
+ * Local state.
+ *
+ * @type {Object}
+ */
+ state = {
+ response: null
+ };
+
+ /**
+ * Lifecycle hook.
+ *
+ * @return {void}
+ */
+ componentDidMount() {
+ this.isStillMounted = true;
+
+ // Do the initial rendering.
+ this.fetch( this.props );
+
+ // Only debounce once the initial fetch occurs to ensure that the first
+ // renders show data as soon as possible.
+ this.fetch = debounce( this.fetch, 500 );
+ }
+
+ /**
+ * Lifecycle hook.
+ *
+ * @return {void}
+ */
+ componentWillUnmount() {
+ this.isStillMounted = false;
+ }
+
+ /**
+ * Lifecycle hook.
+ *
+ * @param {Object} prevProps
+ * @return {void}
+ */
+ componentDidUpdate( prevProps ) {
+ if ( ! isEqual( prevProps, this.props ) ) {
+ this.fetch( this.props );
+ }
+ }
+
+ /**
+ * Fetch the preview of the block.
+ *
+ * @param {Object} props
+ * @return {void}
+ */
+ fetch( props ) {
+ if ( ! this.isStillMounted ) {
+ return;
+ }
+
+ if ( null !== this.state.response ) {
+ this.setState( { response: null } );
+ }
+
+ const { block } = props;
+
+ // Store the latest fetch request so that when we process it, we can
+ // check if it is the current request, to avoid race conditions on slow networks.
+ const fetchRequest = this.currentFetchRequest = apiFetch( {
+ method: 'post',
+ path: '/carbon-fields/v1/block-renderer',
+ data: {
+ name: block.name,
+ content: serialize( [ block ] )
+ }
+ } )
+ .then( ( response ) => {
+ if ( this.isStillMounted && fetchRequest === this.currentFetchRequest && response && response.rendered ) {
+ this.setState( {
+ response: response.rendered
+ } );
+ }
+ } )
+ .catch( ( error ) => {
+ if ( this.isStillMounted && fetchRequest === this.currentFetchRequest ) {
+ this.setState( {
+ response: {
+ error: true,
+ errorMsg: error.message
+ }
+ } );
+ }
+ } );
+ }
+
+ /**
+ * Render the component.
+ *
+ * @return {Object}
+ */
+ render() {
+ const { response } = this.state;
+ const { className } = this.props;
+
+ if ( ! response ) {
+ return (
+
+
+
+ );
+ } else if ( response.error ) {
+ return (
+
+ { sprintf( __( 'Error loading block: %s', 'carbon-fields-ui' ), response.errorMsg ) }
+
+ );
+ } else if ( ! response.length ) {
+ return (
+
+ { __( 'No results found.', 'carbon-fields-ui' ) }
+
+ );
+ }
+
+ return (
+
+ { response }
+
+ );
+ }
+}
+
+export default withSelect( ( select, { clientId } ) => {
+ const { getBlock } = select( 'core/editor' );
+
+ return {
+ block: getBlock( clientId )
+ };
+} )( ServerSideRender );
diff --git a/packages/blocks/index.js b/packages/blocks/index.js
index db22a67f5..583df01fc 100644
--- a/packages/blocks/index.js
+++ b/packages/blocks/index.js
@@ -44,6 +44,7 @@ get( window.cf, 'preloaded.blocks', [] ).forEach( ( container ) => {
registerBlockType( `carbon-fields/${ name }`, {
title: container.title,
icon: getBlockSetting( 'icon' ),
+ parent: getBlockSetting( 'parent', [] ),
category: getBlockSetting( 'category.slug' ),
keywords: getBlockSetting( 'keywords', [] ),
description: getBlockSetting( 'description', '' ),
@@ -56,6 +57,7 @@ get( window.cf, 'preloaded.blocks', [] ).forEach( ( container ) => {
supports: {
tabs: isPlainObject( getBlockSetting( 'tabs' ) ),
preview: getBlockSetting( 'preview' ),
+ innerBlocks: getBlockSetting( 'inner_blocks.enabled' ),
alignWide: false,
anchor: false,
html: false
diff --git a/packages/core/fields/association/index.js b/packages/core/fields/association/index.js
index 6e0a12cb0..c15fd288a 100644
--- a/packages/core/fields/association/index.js
+++ b/packages/core/fields/association/index.js
@@ -229,7 +229,7 @@ class AssociationField extends Component {
-
+
{ (
! option.disabled
diff --git a/packages/core/fields/color/index.js b/packages/core/fields/color/index.js
index 2a0b6a85d..93116a921 100644
--- a/packages/core/fields/color/index.js
+++ b/packages/core/fields/color/index.js
@@ -10,7 +10,7 @@ import { get } from 'lodash';
*/
import './style.scss';
import Picker from './picker';
-import hexToRgba from '../../utils/hex-to-rgba';
+import { hexToRgba, rgbaToHex } from '../../utils/hex-and-rgba';
class ColorField extends Component {
/**
@@ -27,7 +27,7 @@ class ColorField extends Component {
*
* @return {void}
*/
- getBackgrounColor = () => {
+ getBackgroundColor = () => {
const { field, value } = this.props;
const colorHex = value ? value : '#FFFFFFFF';
@@ -44,9 +44,15 @@ class ColorField extends Component {
* @return {void}
*/
handleChange = ( color ) => {
- const { id, onChange } = this.props;
+ const { id, onChange, field } = this.props;
- onChange( id, get( color, 'hex', '' ) );
+ let value = get( color, 'hex', '' ).toUpperCase();
+
+ if ( field.alphaEnabled ) {
+ value = rgbaToHex( get( color, 'rgb', null ) );
+ }
+
+ onChange( id, value );
}
/**
@@ -80,7 +86,7 @@ class ColorField extends Component {
/>