From 9ee560690c5c7504962478616abd7c9357b69680 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 29 Mar 2019 16:15:53 -0400 Subject: [PATCH] Block Library: Reimplement Reusable Block using EditorProvider for embedded post editor --- .../src/block/edit-panel/index.js | 37 ++- packages/block-library/src/block/edit.js | 264 +++++++++--------- packages/block-library/src/block/editor.scss | 9 + .../src/block/selection-observer/index.js | 54 ++++ packages/block-library/src/style.scss | 1 + 5 files changed, 225 insertions(+), 140 deletions(-) create mode 100644 packages/block-library/src/block/editor.scss create mode 100644 packages/block-library/src/block/selection-observer/index.js diff --git a/packages/block-library/src/block/edit-panel/index.js b/packages/block-library/src/block/edit-panel/index.js index eb620a877b63a1..a88bb8b3bddb6b 100644 --- a/packages/block-library/src/block/edit-panel/index.js +++ b/packages/block-library/src/block/edit-panel/index.js @@ -5,7 +5,8 @@ import { Button } from '@wordpress/components'; import { Component, Fragment, createRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { ESCAPE } from '@wordpress/keycodes'; -import { withInstanceId } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { withInstanceId, compose } from '@wordpress/compose'; class ReusableBlockEditPanel extends Component { constructor() { @@ -107,4 +108,36 @@ class ReusableBlockEditPanel extends Component { } } -export default withInstanceId( ReusableBlockEditPanel ); +export default compose( [ + withInstanceId, + withSelect( ( select ) => { + const { getEditedPostAttribute, isSavingPost } = select( 'core/editor' ); + + return { + title: getEditedPostAttribute( 'title' ), + isSaving: isSavingPost(), + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { + editPost, + savePost, + clearSelectedBlock, + } = dispatch( 'core/editor' ); + + return { + onChangeTitle( title ) { + editPost( { title } ); + }, + onSave() { + clearSelectedBlock(); + savePost(); + ownProps.onSave(); + }, + onCancel() { + clearSelectedBlock(); + ownProps.onCancel(); + }, + }; + } ), +] )( ReusableBlockEditPanel ); diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index 722aad97c9bfcc..8146170021adf4 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -1,16 +1,23 @@ -/** - * External dependencies - */ -import { noop, partial } from 'lodash'; - /** * WordPress dependencies */ -import { Component, Fragment } from '@wordpress/element'; -import { Placeholder, Spinner, Disabled } from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; +import { Component } from '@wordpress/element'; +import { Placeholder, Spinner, Disabled, SlotFillProvider } from '@wordpress/components'; +import { + withSelect, + withDispatch, + withRegistry, +} from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { BlockEdit } from '@wordpress/block-editor'; +import { + BlockList, + BlockControls, + BlockFormatControls, + FormatToolbar, + __experimentalBlockSettingsMenuFirstItem, + __experimentalBlockSettingsMenuPluginsExtension, +} from '@wordpress/block-editor'; +import { EditorProvider } from '@wordpress/editor'; import { compose } from '@wordpress/compose'; /** @@ -18,170 +25,151 @@ import { compose } from '@wordpress/compose'; */ import ReusableBlockEditPanel from './edit-panel'; import ReusableBlockIndicator from './indicator'; +import SelectionObserver from './selection-observer'; class ReusableBlockEdit extends Component { - constructor( { reusableBlock } ) { + constructor() { super( ...arguments ); - this.startEditing = this.startEditing.bind( this ); - this.stopEditing = this.stopEditing.bind( this ); - this.setAttributes = this.setAttributes.bind( this ); - this.setTitle = this.setTitle.bind( this ); - this.save = this.save.bind( this ); - - if ( reusableBlock && reusableBlock.isTemporary ) { - // Start in edit mode when we're working with a newly created reusable block - this.state = { - isEditing: true, - title: reusableBlock.title, - changedAttributes: {}, - }; - } else { - // Start in preview mode when we're working with an existing reusable block - this.state = { - isEditing: false, - title: null, - changedAttributes: null, - }; - } - } - - componentDidMount() { - if ( ! this.props.reusableBlock ) { - this.props.fetchReusableBlock(); - } - } - - startEditing() { - const { reusableBlock } = this.props; - - this.setState( { - isEditing: true, - title: reusableBlock.title, - changedAttributes: {}, - } ); - } + this.startEditing = () => this.toggleIsEditing( true ); + this.stopEditing = () => this.toggleIsEditing( false ); + this.cancelEditing = this.cancelEditing.bind( this ); - stopEditing() { - this.setState( { + this.state = { + cancelIncrementKey: 0, + // TODO: Check if this needs to consider reusable block being temporary (this was in original PR) isEditing: false, - title: null, - changedAttributes: null, - } ); - } - - setAttributes( attributes ) { - this.setState( ( prevState ) => { - if ( prevState.changedAttributes !== null ) { - return { changedAttributes: { ...prevState.changedAttributes, ...attributes } }; - } - } ); + }; } - setTitle( title ) { - this.setState( { title } ); + /** + * Starts or stops editing, corresponding to the given boolean value. + * + * @param {boolean} isEditing Whether editing mode should be made active. + */ + toggleIsEditing( isEditing ) { + this.setState( { isEditing } ); } - save() { - const { reusableBlock, onUpdateTitle, updateAttributes, block, onSave } = this.props; - const { title, changedAttributes } = this.state; - - if ( title !== reusableBlock.title ) { - onUpdateTitle( title ); - } - - updateAttributes( block.clientId, changedAttributes ); - onSave(); - + /** + * Stops editing and restores the reusable block to its original saved + * state. + */ + cancelEditing() { this.stopEditing(); + + // Cancelling takes effect by assigning a new key for the rendered + // EditorProvider which forces a re-mount to reset editing state. + let { cancelIncrementKey } = this.state; + cancelIncrementKey++; + this.setState( { cancelIncrementKey } ); } render() { - const { isSelected, reusableBlock, block, isFetching, isSaving, canUpdateBlock } = this.props; - const { isEditing, title, changedAttributes } = this.state; - - if ( ! reusableBlock && isFetching ) { - return ; - } - - if ( ! reusableBlock || ! block ) { - return { __( 'Block has been deleted or is unavailable.' ) }; + const { + isSelected, + reusableBlock, + isFetching, + canUpdateBlock, + settings, + } = this.props; + const { cancelIncrementKey, isEditing } = this.state; + + if ( ! reusableBlock ) { + return ( + + { + isFetching ? + : + __( 'Block has been deleted or is unavailable.' ) + } + + ); } - let element = ( - - ); - + let list = ; if ( ! isEditing ) { - element = { element }; + list = { list }; } return ( - - { ( isSelected || isEditing ) && ( - + + - ) } - { ! isSelected && ! isEditing && } - { element } - + { ( isSelected || isEditing ) && ( + + ) } + { ! isSelected && ! isEditing && ( + + ) } + { list } + + ); } } export default compose( [ + withRegistry, withSelect( ( select, ownProps ) => { - const { - __experimentalGetReusableBlock: getReusableBlock, - __experimentalIsFetchingReusableBlock: isFetchingReusableBlock, - __experimentalIsSavingReusableBlock: isSavingReusableBlock, - } = select( 'core/editor' ); - const { canUser } = select( 'core' ); - const { - getBlock, - } = select( 'core/block-editor' ); - const { ref } = ownProps.attributes; - const reusableBlock = getReusableBlock( ref ); + const { clientId, attributes } = ownProps; + const { ref } = attributes; + const { canUser, getEntityRecord } = select( 'core' ); + const { isResolving } = select( 'core/data' ); + const { getEditorSettings } = select( 'core/editor' ); + const { isBlockSelected } = select( 'core/block-editor' ); + + const isTemporaryReusableBlock = ! Number.isFinite( ref ); + + let reusableBlock; + if ( ! isTemporaryReusableBlock ) { + reusableBlock = getEntityRecord( 'postType', 'wp_block', ref ); + } return { reusableBlock, - isFetching: isFetchingReusableBlock( ref ), - isSaving: isSavingReusableBlock( ref ), - block: reusableBlock ? getBlock( reusableBlock.clientId ) : null, - canUpdateBlock: !! reusableBlock && ! reusableBlock.isTemporary && !! canUser( 'update', 'blocks', ref ), + isSelected: isBlockSelected( clientId ), + isFetching: isResolving( + 'core', + 'getEntityRecord', + [ 'postType', 'wp_block', ref ] + ), + canUpdateBlock: ( + !! reusableBlock && + ! isTemporaryReusableBlock && + !! canUser( 'update', 'blocks', ref ) + ), + settings: getEditorSettings(), }; } ), withDispatch( ( dispatch, ownProps ) => { - const { - __experimentalFetchReusableBlocks: fetchReusableBlocks, - __experimentalUpdateReusableBlockTitle: updateReusableBlockTitle, - __experimentalSaveReusableBlock: saveReusableBlock, - } = dispatch( 'core/editor' ); - const { - updateBlockAttributes, - } = dispatch( 'core/block-editor' ); - const { ref } = ownProps.attributes; + const { selectBlock } = dispatch( 'core/block-editor' ); return { - fetchReusableBlock: partial( fetchReusableBlocks, ref ), - updateAttributes: updateBlockAttributes, - onUpdateTitle: partial( updateReusableBlockTitle, ref ), - onSave: partial( saveReusableBlock, ref ), + selectBlock() { + selectBlock( ownProps.clientId ); + }, }; } ), ] )( ReusableBlockEdit ); diff --git a/packages/block-library/src/block/editor.scss b/packages/block-library/src/block/editor.scss new file mode 100644 index 00000000000000..7523878adf2d86 --- /dev/null +++ b/packages/block-library/src/block/editor.scss @@ -0,0 +1,9 @@ +.block-editor-block-list__block[data-type="core/block"] { + .block-editor-block-list__layout > .block-editor-block-list__block:first-child > .block-editor-block-list__block-edit { + margin-top: 0; + } + + .block-editor-block-list__layout > .block-editor-block-list__block:last-child > .block-editor-block-list__block-edit { + margin-bottom: 0; + } +} diff --git a/packages/block-library/src/block/selection-observer/index.js b/packages/block-library/src/block/selection-observer/index.js new file mode 100644 index 00000000000000..e95bc80e525cbe --- /dev/null +++ b/packages/block-library/src/block/selection-observer/index.js @@ -0,0 +1,54 @@ +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { compose } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; + +/** + * Component which calls onBlockSelected prop when a block becomes selected. It + * is assumed to be used in a separate registry context from the reusable block + * in which it is rendered, ensuring that only one block appears as selected + * between the editor in which the reusable resides and block's own editor. + * + * @type {WPComponent} + */ +class SelectionObserver extends Component { + componentDidUpdate( prevProps ) { + const { + hasSelectedBlock, + onBlockSelected, + isParentSelected, + clearSelectedBlock, + } = this.props; + + if ( hasSelectedBlock && ! prevProps.hasSelectedBlock ) { + onBlockSelected(); + } + + if ( ! isParentSelected && prevProps.isParentSelected ) { + clearSelectedBlock(); + } + } + + render() { + return null; + } +} + +export default compose( [ + withSelect( ( select ) => { + const { hasSelectedBlock } = select( 'core/block-editor' ); + + return { + hasSelectedBlock: hasSelectedBlock(), + }; + } ), + withDispatch( ( dispatch ) => { + const { clearSelectedBlock } = dispatch( 'core/block-editor' ); + + return { + clearSelectedBlock, + }; + } ), +] )( SelectionObserver ); diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index 5599dafd7c463f..419c42f9f2d9f2 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -1,4 +1,5 @@ @import "./audio/style.scss"; +@import "./block/editor.scss"; @import "./block/edit-panel/style.scss"; @import "./block/indicator/style.scss"; @import "./button/style.scss";