diff --git a/blocks/editable/index.js b/blocks/editable/index.js
index ec0ae2fb5c2b6..8d1529593a76f 100644
--- a/blocks/editable/index.js
+++ b/blocks/editable/index.js
@@ -86,7 +86,7 @@ export default class Editable extends wp.element.Component {
onSetup( editor ) {
this.editor = editor;
editor.on( 'init', this.onInit );
- editor.on( 'focusout', this.onChange );
+ editor.on( 'change', this.onChange );
editor.on( 'NewBlock', this.onNewBlock );
editor.on( 'focusin', this.onFocus );
editor.on( 'nodechange', this.onNodeChange );
@@ -130,8 +130,13 @@ export default class Editable extends wp.element.Component {
}
this.savedContent = this.getContent();
- this.editor.save();
this.props.onChange( this.savedContent );
+
+ // Save contents to the element, but avoid events since by default the
+ // save function will incur another `change` event
+ this.editor.save( {
+ no_events: true,
+ } );
}
getRelativePosition( node ) {
diff --git a/blocks/library/text/index.js b/blocks/library/text/index.js
index 870d260cefe7e..4fcfda89a4c82 100644
--- a/blocks/library/text/index.js
+++ b/blocks/library/text/index.js
@@ -17,10 +17,6 @@ registerBlock( 'core/text', {
content: children(),
},
- defaultAttributes: {
- content:
,
- },
-
merge( attributes, attributesToMerge ) {
return {
content: wp.element.concatChildren( attributes.content, attributesToMerge.content ),
diff --git a/editor/inserter/index.js b/editor/inserter/index.js
index 81ca096d19fd2..b1e7ec15067f8 100644
--- a/editor/inserter/index.js
+++ b/editor/inserter/index.js
@@ -3,6 +3,7 @@
*/
import clickOutside from 'react-click-outside';
import { connect } from 'react-redux';
+import classnames from 'classnames';
/**
* WordPress dependencies
@@ -65,10 +66,11 @@ class Inserter extends wp.element.Component {
render() {
const { opened } = this.state;
- const { position } = this.props;
+ const { position, className } = this.props;
+ const classes = classnames( 'editor-inserter', className );
return (
-
+
( {
+ ...stateProps,
+ ...dispatchProps,
+ ...ownProps,
} )
)( clickOutside( Inserter ) );
diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js
index 788f98189ccfa..3040c04726c89 100644
--- a/editor/modes/visual-editor/block.js
+++ b/editor/modes/visual-editor/block.js
@@ -9,6 +9,7 @@ import { partial } from 'lodash';
/**
* WordPress dependencies
*/
+import { createBlock } from 'blocks';
import Toolbar from 'components/toolbar';
/**
@@ -16,11 +17,13 @@ import Toolbar from 'components/toolbar';
*/
import BlockMover from '../../block-mover';
import BlockSwitcher from '../../block-switcher';
+import Inserter from '../../inserter';
import {
getPreviousBlock,
getBlock,
getBlockFocus,
getBlockOrder,
+ isNewBlock,
isBlockHovered,
isBlockSelected,
isTypingInBlock,
@@ -36,6 +39,7 @@ class VisualEditorBlock extends wp.element.Component {
this.maybeStartTyping = this.maybeStartTyping.bind( this );
this.removeOnBackspace = this.removeOnBackspace.bind( this );
this.mergeWithPrevious = this.mergeWithPrevious.bind( this );
+ this.replaceNewBlock = this.replaceNewBlock.bind( this );
this.previousOffset = null;
}
@@ -146,6 +150,13 @@ class VisualEditorBlock extends wp.element.Component {
);
}
+ replaceNewBlock( slug ) {
+ // When choosing block from inserter for a new empty block, override
+ // insert behavior to replace current block instead
+ const { uid, replaceBlocks } = this.props;
+ replaceBlocks( [ uid ], [ createBlock( slug ) ] );
+ }
+
componentDidUpdate( prevProps ) {
if ( this.previousOffset ) {
window.scrollTo(
@@ -180,7 +191,7 @@ class VisualEditorBlock extends wp.element.Component {
return null;
}
- const { isHovered, isSelected, isTyping, focus } = this.props;
+ const { isHovered, isSelected, isNew, isTyping, focus } = this.props;
const className = classnames( 'editor-visual-editor__block', {
'is-selected': isSelected && ! isTyping,
'is-hovered': isHovered,
@@ -212,7 +223,14 @@ class VisualEditorBlock extends wp.element.Component {
tabIndex="0"
{ ...wrapperProps }
>
- { ( ( isSelected && ! isTyping ) || isHovered ) && }
+ { isNew && isSelected && (
+
+ ) }
+ { ! isNew && ( ( isSelected && ! isTyping ) || isHovered ) && (
+
+ ) }
{ isSelected && ! isTyping &&
@@ -260,6 +278,7 @@ export default connect(
block: getBlock( state, ownProps.uid ),
isSelected: isBlockSelected( state, ownProps.uid ),
isHovered: isBlockHovered( state, ownProps.uid ),
+ isNew: isNewBlock( state, ownProps.uid ),
focus: getBlockFocus( state, ownProps.uid ),
isTyping: isTypingInBlock( state, ownProps.uid ),
order: getBlockOrder( state, ownProps.uid ),
diff --git a/editor/modes/visual-editor/index.js b/editor/modes/visual-editor/index.js
index 7e85e8dbb6d1f..aa8280fe81cba 100644
--- a/editor/modes/visual-editor/index.js
+++ b/editor/modes/visual-editor/index.js
@@ -19,7 +19,9 @@ function VisualEditor( { blocks } ) {
{ blocks.map( ( uid ) => (
) ) }
-
+
);
}
diff --git a/editor/modes/visual-editor/style.scss b/editor/modes/visual-editor/style.scss
index 5e4793d7ec90b..c935c51eefca6 100644
--- a/editor/modes/visual-editor/style.scss
+++ b/editor/modes/visual-editor/style.scss
@@ -19,7 +19,8 @@
}
/* "Hassle-free full bleed" from CSS Tricks */
-.editor-visual-editor > *:not( [data-align="wide"] ) {
+.editor-post-title,
+.editor-visual-editor__block:not( [data-align="wide"] ) {
max-width: $visual-editor-max-width;
margin-left: auto;
margin-right: auto;
@@ -107,6 +108,16 @@
display: inline-flex;
}
-.editor-visual-editor .editor-inserter {
+.editor-visual-editor__empty-block-inserter {
+ position: absolute;
+ top: 10px;
+ left: -10px;
+
+ &:not( :hover ) {
+ opacity: 0.8;
+ }
+}
+
+.editor-visual-editor__inserter {
margin: $item-spacing $item-spacing $item-spacing calc( 50% - #{ $visual-editor-max-width / 2 } ); // account for full-width trick
}
diff --git a/editor/selectors.js b/editor/selectors.js
index ddacc7b185124..ee1e08fd58345 100644
--- a/editor/selectors.js
+++ b/editor/selectors.js
@@ -69,6 +69,32 @@ export function getBlockOrder( state, uid ) {
return state.editor.blockOrder.indexOf( uid );
}
+/**
+ * Returns true if the block is empty, null if the block is not known, or null
+ * otherwise.
+ *
+ * @param {Object} state Current application state
+ * @param {string} uid Block UID
+ * @return {?Boolean} Whether block is empty, or null if unknown
+ */
+export function isNewBlock( state, uid ) {
+ const block = getBlock( state, uid );
+ if ( ! block ) {
+ return null;
+ }
+
+ // A block is considered new if it's a text block without content. Usually
+ // we'd avoid engrained knowledge of specific block types, but the behavior
+ // of text as the default new inserted block is a special case. Regardless,
+ // that we abstract this behind a generic selector enables us to refactor
+ // in the future to one with fewer specific implementation details.
+
+ return (
+ 'core/text' === block.blockType &&
+ ! block.attributes.content
+ );
+}
+
export function isFirstBlock( state, uid ) {
return first( state.editor.blockOrder ) === uid;
}
diff --git a/editor/test/selectors.js b/editor/test/selectors.js
index d1d7c3d7ab323..3b24a4ae99ba3 100644
--- a/editor/test/selectors.js
+++ b/editor/test/selectors.js
@@ -20,6 +20,7 @@ import {
getBlocks,
getBlockUids,
getBlockOrder,
+ isNewBlock,
isFirstBlock,
isLastBlock,
getPreviousBlock,
@@ -267,6 +268,52 @@ describe( 'selectors', () => {
} );
} );
+ describe( 'isNewBlock()', () => {
+ it( 'returns null if unknown', () => {
+ const state = {
+ editor: {
+ blocksByUid: {},
+ },
+ };
+
+ expect( isNewBlock( state, 23 ) ).to.be.null();
+ } );
+
+ it( 'returns true if new', () => {
+ const state = {
+ editor: {
+ blocksByUid: {
+ 23: {
+ blockType: 'core/text',
+ attributes: {
+ content: undefined,
+ },
+ },
+ },
+ },
+ };
+
+ expect( isNewBlock( state, 23 ) ).to.be.true();
+ } );
+
+ it( 'returns false if not new', () => {
+ const state = {
+ editor: {
+ blocksByUid: {
+ 23: {
+ blockType: 'core/text',
+ attributes: {
+ content: {},
+ },
+ },
+ },
+ },
+ };
+
+ expect( isNewBlock( state, 23 ) ).to.be.false();
+ } );
+ } );
+
describe( 'isFirstBlock', () => {
it( 'should return true when the block is first', () => {
const state = {