From 679b677082cc92f9a77ff7ec1e7ac4f9b511bc3a Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 29 Mar 2019 16:11:06 -0400 Subject: [PATCH 1/4] Block Editor: Define RichText FormatToolbar as abstracted component --- packages/block-editor/README.md | 4 ++++ packages/block-editor/src/components/index.js | 1 + .../src/components/rich-text/format-toolbar/index.js | 10 +++++++--- .../rich-text/format-toolbar/index.native.js | 8 ++++++-- .../block-editor/src/components/rich-text/index.js | 1 + 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index ff1a8579a0b01..d9fc07588e843 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -197,6 +197,10 @@ Undocumented declaration. Undocumented declaration. +# **FormatToolbar** + +Undocumented declaration. + # **getColorClassName** Returns a class based on the context a color is being used and its slug. diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index dc38643a6519b..80f5c1f426d15 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -29,6 +29,7 @@ export { RichTextShortcut, RichTextToolbarButton, __unstableRichTextInputEvent, + FormatToolbar, } from './rich-text'; export { default as URLInput } from './url-input'; export { default as URLInputButton } from './url-input/button'; diff --git a/packages/block-editor/src/components/rich-text/format-toolbar/index.js b/packages/block-editor/src/components/rich-text/format-toolbar/index.js index aa6570f620035..2296d3a37f396 100644 --- a/packages/block-editor/src/components/rich-text/format-toolbar/index.js +++ b/packages/block-editor/src/components/rich-text/format-toolbar/index.js @@ -9,7 +9,9 @@ import { orderBy } from 'lodash'; */ import { __ } from '@wordpress/i18n'; -import { Toolbar, Slot, DropdownMenu } from '@wordpress/components'; +import { Toolbar, Slot, DropdownMenu, createSlotFill } from '@wordpress/components'; + +const { Slot: ToolbarControlsSlot } = createSlotFill( 'RichText.ToolbarControls' ); const FormatToolbar = ( { controls } ) => { return ( @@ -18,7 +20,7 @@ const FormatToolbar = ( { controls } ) => { { controls.map( ( format ) => ) } - + { ( fills ) => fills.length !== 0 && { controls={ orderBy( fills.map( ( [ { props } ] ) => props ), 'title' ) } /> } - + ); }; +FormatToolbar.Slot = ToolbarControlsSlot; + export default FormatToolbar; diff --git a/packages/block-editor/src/components/rich-text/format-toolbar/index.native.js b/packages/block-editor/src/components/rich-text/format-toolbar/index.native.js index d90860c05f4d6..99294ba268192 100644 --- a/packages/block-editor/src/components/rich-text/format-toolbar/index.native.js +++ b/packages/block-editor/src/components/rich-text/format-toolbar/index.native.js @@ -2,7 +2,9 @@ * WordPress dependencies */ -import { Toolbar, Slot } from '@wordpress/components'; +import { Toolbar, Slot, createSlotFill } from '@wordpress/components'; + +const { Slot: ToolbarControlsSlot } = createSlotFill( 'RichText.ToolbarControls' ); const FormatToolbar = ( { controls } ) => { return ( @@ -10,9 +12,11 @@ const FormatToolbar = ( { controls } ) => { { controls.map( ( format ) => ) } - + ); }; +FormatToolbar.Slot = ToolbarControlsSlot; + export default FormatToolbar; diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 40f758f064e08..a98fd82c99967 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -1217,3 +1217,4 @@ export default RichTextContainer; export { RichTextShortcut } from './shortcut'; export { RichTextToolbarButton } from './toolbar-button'; export { __unstableRichTextInputEvent } from './input-event'; +export { FormatToolbar }; From da4e149b2cc351905deecda9057f2d4126e7600b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 29 Mar 2019 16:11:54 -0400 Subject: [PATCH 2/4] Components: Add slots proxying support to SlotFillProvider --- packages/components/src/slot-fill/context.js | 80 ++++++++++++++++---- packages/components/src/slot-fill/fill.js | 19 +---- packages/components/src/slot-fill/index.js | 1 + packages/components/src/slot-fill/slot.js | 19 +---- 4 files changed, 71 insertions(+), 48 deletions(-) diff --git a/packages/components/src/slot-fill/context.js b/packages/components/src/slot-fill/context.js index 7ee0d9806c400..d65f93f722f1a 100644 --- a/packages/components/src/slot-fill/context.js +++ b/packages/components/src/slot-fill/context.js @@ -1,12 +1,13 @@ /** * External dependencies */ -import { sortBy, forEach, without } from 'lodash'; +import { sortBy, forEach, without, some } from 'lodash'; /** * WordPress dependencies */ import { Component, createContext } from '@wordpress/element'; +import { createHigherOrderComponent } from '@wordpress/compose'; const { Provider, Consumer } = createContext( { registerSlot: () => {}, @@ -21,22 +22,49 @@ class SlotFillProvider extends Component { constructor() { super( ...arguments ); - this.registerSlot = this.registerSlot.bind( this ); - this.registerFill = this.registerFill.bind( this ); - this.unregisterSlot = this.unregisterSlot.bind( this ); - this.unregisterFill = this.unregisterFill.bind( this ); - this.getSlot = this.getSlot.bind( this ); - this.getFills = this.getFills.bind( this ); - this.slots = {}; this.fills = {}; this.state = { - registerSlot: this.registerSlot, - unregisterSlot: this.unregisterSlot, - registerFill: this.registerFill, - unregisterFill: this.unregisterFill, - getSlot: this.getSlot, - getFills: this.getFills, + registerSlot: this.proxy( 'registerSlot' ).bind( this ), + registerFill: this.proxy( 'registerFill' ).bind( this ), + unregisterSlot: this.proxy( 'unregisterSlot' ).bind( this ), + unregisterFill: this.proxy( 'unregisterFill' ).bind( this ), + getSlot: this.proxy( 'getSlot' ).bind( this ), + getFills: this.proxy( 'getFills' ).bind( this ), + }; + } + + /** + * Given a function name for a function on the SlotFillProvider prototype, + * returns a new function which either passes the arguments to the current + * instance, or to the context ancestor, dependent on whether the provider + * is configured to handle the given slot. + * + * @param {string} functionName SlotFillProvider function name. + * + * @return {Function} Proxying function. + */ + proxy( functionName ) { + return ( name, ...args ) => { + const { slots: handledSlots } = this.props; + + let handler = this[ functionName ]; + + if ( Array.isArray( handledSlots ) ) { + const isHandled = some( handledSlots, ( slotNameOrComponent ) => { + const { slotName = slotNameOrComponent } = slotNameOrComponent; + return typeof slotName === 'string' && ( + slotName === name || + name.startsWith( slotName + '.' ) + ); + } ); + + if ( ! isHandled ) { + handler = this.props[ functionName ]; + } + } + + return handler.call( this, name, ...args ); }; } @@ -129,5 +157,25 @@ class SlotFillProvider extends Component { } } -export default SlotFillProvider; -export { Consumer }; +const withConsumerContext = createHigherOrderComponent( + ( WrappedComponent ) => ( props ) => ( + + { ( context ) => ( + + ) } + + ), + 'withConsumerContext' +); + +export default withConsumerContext( SlotFillProvider ); + +export { Consumer, withConsumerContext }; diff --git a/packages/components/src/slot-fill/fill.js b/packages/components/src/slot-fill/fill.js index a0c2876643f04..b60f4ff45dc91 100644 --- a/packages/components/src/slot-fill/fill.js +++ b/packages/components/src/slot-fill/fill.js @@ -11,11 +11,11 @@ import { createPortal, useLayoutEffect, useRef, useState } from '@wordpress/elem /** * Internal dependencies */ -import { Consumer } from './context'; +import { withConsumerContext } from './context'; let occurrences = 0; -function FillComponent( { name, getSlot, children, registerFill, unregisterFill } ) { +function Fill( { name, getSlot, children, registerFill, unregisterFill } ) { // Random state used to rerender the component if needed, ideally we don't need this const [ , updateRerenderState ] = useState( {} ); const rerender = () => updateRerenderState( {} ); @@ -67,17 +67,4 @@ function FillComponent( { name, getSlot, children, registerFill, unregisterFill return createPortal( children, slot.node ); } -const Fill = ( props ) => ( - - { ( { getSlot, registerFill, unregisterFill } ) => ( - - ) } - -); - -export default Fill; +export default withConsumerContext( Fill ); diff --git a/packages/components/src/slot-fill/index.js b/packages/components/src/slot-fill/index.js index 4155b083b0fdf..8460c913cf718 100644 --- a/packages/components/src/slot-fill/index.js +++ b/packages/components/src/slot-fill/index.js @@ -15,6 +15,7 @@ export function createSlotFill( name ) { const SlotComponent = ( props ) => ; SlotComponent.displayName = name + 'Slot'; + SlotComponent.slotName = name; return { Fill: FillComponent, diff --git a/packages/components/src/slot-fill/slot.js b/packages/components/src/slot-fill/slot.js index 6de69f74f865a..25c7c7f97f2ca 100644 --- a/packages/components/src/slot-fill/slot.js +++ b/packages/components/src/slot-fill/slot.js @@ -21,9 +21,9 @@ import { /** * Internal dependencies */ -import { Consumer } from './context'; +import { withConsumerContext } from './context'; -class SlotComponent extends Component { +class Slot extends Component { constructor() { super( ...arguments ); @@ -89,17 +89,4 @@ class SlotComponent extends Component { } } -const Slot = ( props ) => ( - - { ( { registerSlot, unregisterSlot, getFills } ) => ( - - ) } - -); - -export default Slot; +export default withConsumerContext( Slot ); From cae7bb735581cbf71b503e40d6cebf7356d82d62 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 29 Mar 2019 16:15:25 -0400 Subject: [PATCH 3/4] Edit Post: Remove SlotFillProvider as rendered by block editor --- .../src/components/provider/index.js | 10 +++---- packages/edit-post/src/editor.js | 30 ++++++++++--------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 43f336325434d..018b639eddb5d 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { DropZoneProvider, SlotFillProvider } from '@wordpress/components'; +import { DropZoneProvider } from '@wordpress/components'; import { withDispatch } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -121,11 +121,9 @@ class BlockEditorProvider extends Component { const { children } = this.props; return ( - - - { children } - - + + { children } + ); } } diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index ea48a3a1d8e35..0d63500f33885 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -10,7 +10,7 @@ import { size, map, without } from 'lodash'; import { withSelect } from '@wordpress/data'; import { EditorProvider, ErrorBoundary, PostLockedModal } from '@wordpress/editor'; import { StrictMode, Component } from '@wordpress/element'; -import { KeyboardShortcuts } from '@wordpress/components'; +import { KeyboardShortcuts, SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies @@ -87,19 +87,21 @@ class Editor extends Component { return ( - - - - - - - + + + + + + + + + ); } From edaf84fb09b611d34c56722ac65a71deba463fd4 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 29 Mar 2019 16:15:53 -0400 Subject: [PATCH 4/4] 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 | 262 +++++++++--------- 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, 224 insertions(+), 139 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 0bc064d25203c..7620e8857d949 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, 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 785f5a762c01f..8146170021adf 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 } from '@wordpress/element'; -import { Placeholder, Spinner, Disabled } from '@wordpress/components'; -import { withSelect, withDispatch } from '@wordpress/data'; +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 0000000000000..7523878adf2d8 --- /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 0000000000000..e95bc80e525cb --- /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 f9a2909ff360a..0a97874c6c005 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 "./button/style.scss"; @import "./calendar/style.scss"; @import "./categories/style.scss";