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 = {