-
Notifications
You must be signed in to change notification settings - Fork 4.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add reusable blocks #2659
Add reusable blocks #2659
Changes from all commits
6e3431f
6721b27
7f6dd91
ade0f06
16588b8
e9b4701
23162e3
fee015f
51f8a38
d5c0c49
a6cd1dd
80ee61f
9fdcb50
67d0ec6
2eb4f96
6e5876e
256ddbb
2f89fea
2822984
38887bb
735e2c0
3d3affb
4448ea2
f587eed
4b7f597
9f0afb9
baf43b3
6cf3950
cd03ad3
49ec18c
fcdf318
e8c33c1
69e5705
7fdb1d8
003c900
1254e97
6faec68
5637827
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Button, withInstanceId } from '@wordpress/components'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import BaseControl from './../base-control'; | ||
import './style.scss'; | ||
|
||
function ButtonControl( { instanceId, label, value, help, ...props } ) { | ||
const id = 'inspector-button-control-' + instanceId; | ||
return ( | ||
<BaseControl id={ id } label={ label } help={ help } className="blocks-button-control"> | ||
<Button { ...props } id={ id } isLarge className={ 'blocks-button-control__button' }> | ||
{ value } | ||
</Button> | ||
</BaseControl> | ||
); | ||
} | ||
|
||
export default withInstanceId( ButtonControl ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.blocks-button-control .blocks-button-control__button { | ||
margin-bottom: 0.5em; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,3 +22,4 @@ import './text-columns'; | |
import './verse'; | ||
import './video'; | ||
import './audio'; | ||
import './reusable-block'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Button } from '@wordpress/components'; | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import './style.scss'; | ||
|
||
function ReusableBlockEditPanel( props ) { | ||
const { isEditing, name, isSaving, onEdit, onDetach, onChangeName, onSave, onCancel } = props; | ||
|
||
return ( | ||
<div className="reusable-block-edit-panel"> | ||
{ ! isEditing && ! isSaving && [ | ||
<span key="info" className="reusable-block-edit-panel__info"> | ||
<b>{ name }</b> | ||
</span>, | ||
<Button | ||
key="edit" | ||
isLarge | ||
className="reusable-block-edit-panel__button" | ||
onClick={ onEdit }> | ||
{ __( 'Edit' ) } | ||
</Button>, | ||
<Button | ||
key="detach" | ||
isLarge | ||
className="reusable-block-edit-panel__button" | ||
onClick={ onDetach }> | ||
{ __( 'Detach' ) } | ||
</Button>, | ||
] } | ||
{ ( isEditing || isSaving ) && [ | ||
<input | ||
key="name" | ||
type="text" | ||
disabled={ isSaving } | ||
className="reusable-block-edit-panel__name" | ||
value={ name } | ||
onChange={ ( event ) => onChangeName( event.target.value ) } />, | ||
<Button | ||
key="save" | ||
isPrimary | ||
isLarge | ||
isIndicatingProgress={ isSaving } | ||
disabled={ ! name || isSaving } | ||
className="reusable-block-edit-panel__button" | ||
onClick={ onSave }> | ||
{ __( 'Save' ) } | ||
</Button>, | ||
<Button | ||
key="cancel" | ||
isLarge | ||
disabled={ isSaving } | ||
className="reusable-block-edit-panel__button" | ||
onClick={ onCancel }> | ||
{ __( 'Cancel' ) } | ||
</Button>, | ||
] } | ||
</div> | ||
); | ||
} | ||
|
||
export default ReusableBlockEditPanel; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
.reusable-block-edit-panel { | ||
align-items: center; | ||
background: $light-gray-100; | ||
color: $dark-gray-500; | ||
display: flex; | ||
font-family: $default-font; | ||
font-size: $default-font-size; | ||
justify-content: flex-end; | ||
margin: $block-padding (-$block-padding) (-$block-padding); | ||
padding: 10px $block-padding; | ||
|
||
.reusable-block-edit-panel__spinner { | ||
margin: 0 5px; | ||
} | ||
|
||
.reusable-block-edit-panel__info { | ||
margin-right: auto; | ||
} | ||
|
||
.reusable-block-edit-panel__name { | ||
flex-grow: 1; | ||
font-size: 14px; | ||
height: 30px; | ||
margin: 0 auto 0 0; | ||
max-width: 230px; | ||
} | ||
|
||
// Needs specificity to override the margin-bottom set by .button | ||
.wp-core-ui & .reusable-block-edit-panel__button { | ||
margin: 0 0 0 5px; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { pickBy, noop } from 'lodash'; | ||
import { connect } from 'react-redux'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { Component } from '@wordpress/element'; | ||
import { Placeholder, Spinner } from '@wordpress/components'; | ||
import { __ } from '@wordpress/i18n'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { getBlockType, registerBlockType } from '../../api'; | ||
import ReusableBlockEditPanel from './edit-panel'; | ||
|
||
class ReusableBlockEdit extends Component { | ||
constructor() { | ||
super( ...arguments ); | ||
|
||
this.startEditing = this.startEditing.bind( this ); | ||
this.stopEditing = this.stopEditing.bind( this ); | ||
this.setAttributes = this.setAttributes.bind( this ); | ||
this.setName = this.setName.bind( this ); | ||
this.updateReusableBlock = this.updateReusableBlock.bind( this ); | ||
|
||
this.state = { | ||
isEditing: false, | ||
name: null, | ||
attributes: null, | ||
}; | ||
} | ||
|
||
componentDidMount() { | ||
if ( ! this.props.reusableBlock ) { | ||
this.props.fetchReusableBlock(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My first reaction to seeing this and related Redux actions: Could we simplify this by using the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The major problem with using Also: right now, if you have two instances of the same reusable block in the post, editing one will immediately update the other. We get this for free because the UI is derived from what's in the store. It's a small thing, but kind of nice. |
||
} | ||
} | ||
|
||
startEditing() { | ||
this.setState( { isEditing: true } ); | ||
} | ||
|
||
stopEditing() { | ||
this.setState( { | ||
isEditing: false, | ||
name: null, | ||
attributes: null, | ||
} ); | ||
} | ||
|
||
setAttributes( attributes ) { | ||
this.setState( ( prevState ) => ( { | ||
attributes: { ...prevState.attributes, ...attributes }, | ||
} ) ); | ||
} | ||
|
||
setName( name ) { | ||
this.setState( { name } ); | ||
} | ||
|
||
updateReusableBlock() { | ||
const { name, attributes } = this.state; | ||
|
||
// Use pickBy to include only changed (assigned) values in payload | ||
const payload = pickBy( { | ||
name, | ||
attributes, | ||
} ); | ||
|
||
this.props.updateReusableBlock( payload ); | ||
this.props.saveReusableBlock(); | ||
this.stopEditing(); | ||
} | ||
|
||
render() { | ||
const { focus, reusableBlock, convertBlockToStatic } = this.props; | ||
const { isEditing, name, attributes } = this.state; | ||
|
||
if ( ! reusableBlock ) { | ||
return <Placeholder><Spinner /></Placeholder>; | ||
} | ||
|
||
const blockType = getBlockType( reusableBlock.type ); | ||
const BlockEdit = blockType.edit || blockType.save; | ||
|
||
return [ | ||
// We fake the block being read-only by wrapping it with an element that has pointer-events: none | ||
<div key="edit" style={ { pointerEvents: isEditing ? 'auto' : 'none' } }> | ||
<BlockEdit | ||
{ ...this.props } | ||
focus={ isEditing ? focus : null } | ||
attributes={ { ...reusableBlock.attributes, ...attributes } } | ||
setAttributes={ isEditing ? this.setAttributes : noop } /> | ||
</div>, | ||
focus && ( | ||
<ReusableBlockEditPanel | ||
key="panel" | ||
isEditing={ isEditing } | ||
name={ name !== null ? name : reusableBlock.name } | ||
isSaving={ reusableBlock.isSaving } | ||
onEdit={ this.startEditing } | ||
onDetach={ convertBlockToStatic } | ||
onChangeName={ this.setName } | ||
onSave={ this.updateReusableBlock } | ||
onCancel={ this.stopEditing } /> | ||
), | ||
]; | ||
} | ||
} | ||
|
||
/** | ||
* TODO: | ||
* These should use selectors and action creators that we gather from context. | ||
* OR this entire component should move to `editor`, I can't decide. | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're in the process of merging the two modules (blocks and editor) which will resolve this issue #2795 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! I was dreading how awkward using I'll leave the code as is until #2795 is merged. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
const ConnectedReusableBlockEdit = connect( | ||
( state, ownProps ) => ( { | ||
reusableBlock: state.reusableBlocks[ ownProps.attributes.ref ], | ||
} ), | ||
( dispatch, ownProps ) => ( { | ||
fetchReusableBlock() { | ||
dispatch( { | ||
type: 'FETCH_REUSABLE_BLOCKS', | ||
id: ownProps.attributes.ref, | ||
} ); | ||
}, | ||
updateReusableBlock( reusableBlock ) { | ||
dispatch( { | ||
type: 'UPDATE_REUSABLE_BLOCK', | ||
id: ownProps.attributes.ref, | ||
reusableBlock, | ||
} ); | ||
}, | ||
saveReusableBlock() { | ||
dispatch( { | ||
type: 'SAVE_REUSABLE_BLOCK', | ||
id: ownProps.attributes.ref, | ||
} ); | ||
}, | ||
convertBlockToStatic() { | ||
dispatch( { | ||
type: 'CONVERT_BLOCK_TO_STATIC', | ||
uid: ownProps.id, | ||
} ); | ||
}, | ||
} ) | ||
)( ReusableBlockEdit ); | ||
|
||
registerBlockType( 'core/reusable-block', { | ||
title: __( 'Reusable Block' ), | ||
category: 'reusable-blocks', | ||
isPrivate: true, | ||
|
||
attributes: { | ||
ref: { | ||
type: 'string', | ||
}, | ||
}, | ||
|
||
edit: ConnectedReusableBlockEdit, | ||
save: () => null, | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the instanceId and the id useful in this component? Generally, we're using it to match labels and inputs but there's no input in this component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch.
<button>
elements are labelable, but I wasn't passingid
along to the<Button>
component. I've fixed this in 224eb27f54a397b3c64f3793174fcb9c2e0cd252.