diff --git a/blocks/components/editable/index.js b/blocks/components/editable/index.js index 0ada78bff1301f..0edfba8f520053 100644 --- a/blocks/components/editable/index.js +++ b/blocks/components/editable/index.js @@ -17,6 +17,7 @@ export default class Editable extends wp.element.Component { this.onChange = this.onChange.bind( this ); this.onNewBlock = this.onNewBlock.bind( this ); this.bindNode = this.bindNode.bind( this ); + this.onFocus = this.onFocus.bind( this ); } componentDidMount() { @@ -45,11 +46,22 @@ export default class Editable extends wp.element.Component { editor.on( 'init', this.onInit ); editor.on( 'focusout', this.onChange ); editor.on( 'NewBlock', this.onNewBlock ); + editor.on( 'focusin', this.onFocus ); } onInit() { const { value = '' } = this.props; this.editor.setContent( value ); + this.focus(); + } + + onFocus() { + if ( ! this.props.onFocus ) { + return; + } + + // TODO: We need a way to save the focus position ( bookmark maybe ) + this.props.onFocus(); } onChange() { @@ -107,6 +119,12 @@ export default class Editable extends wp.element.Component { this.editor.selection.moveToBookmark( bookmark ); } + focus() { + if ( this.props.focus ) { + this.editor.focus(); + } + } + componentWillUpdate( nextProps ) { if ( this.editor && this.props.tagName !== nextProps.tagName ) { this.editor.destroy(); @@ -122,7 +140,16 @@ export default class Editable extends wp.element.Component { componentDidUpdate( prevProps ) { if ( this.props.tagName !== prevProps.tagName ) { this.initialize(); - } else if ( this.props.value !== prevProps.value ) { + } + + if ( !! this.props.focus && ! prevProps.focus ) { + this.focus(); + } + + if ( + this.props.tagName === prevProps.tagName && + this.props.value !== prevProps.value + ) { this.updateContent(); } } diff --git a/blocks/library/heading/index.js b/blocks/library/heading/index.js index acb6c5d0de4f2e..e0891c0916ca2b 100644 --- a/blocks/library/heading/index.js +++ b/blocks/library/heading/index.js @@ -31,13 +31,15 @@ registerBlock( 'core/heading', { } ) ) ], - edit( { attributes, setAttributes } ) { + edit( { attributes, setAttributes, focus, setFocus } ) { const { content, tag, align } = attributes; return ( setAttributes( { content: value } ) } style={ align ? { textAlign: align } : null } /> diff --git a/blocks/library/image/index.js b/blocks/library/image/index.js index 1705bbcdd0d8cf..6b93a95e70f0bd 100644 --- a/blocks/library/image/index.js +++ b/blocks/library/image/index.js @@ -19,17 +19,19 @@ registerBlock( 'core/image', { caption: html( 'figcaption' ) }, - edit( { attributes, isSelected, setAttributes } ) { + edit( { attributes, setAttributes, focus, setFocus } ) { const { url, alt, caption } = attributes; return (
{ - { caption || isSelected ? ( + { caption || !! focus ? ( setAttributes( { caption: value } ) } /> ) : null }
diff --git a/blocks/library/list/index.js b/blocks/library/list/index.js index 7b8112444a4f0c..ea98f5ed0c1424 100644 --- a/blocks/library/list/index.js +++ b/blocks/library/list/index.js @@ -54,7 +54,7 @@ registerBlock( 'core/list', { } ], - edit( { attributes } ) { + edit( { attributes, focus, setFocus } ) { const { listType = 'ol', items = [], align } = attributes; const content = items.map( item => { return `
  • ${ item.value }
  • `; @@ -65,6 +65,8 @@ registerBlock( 'core/list', { tagName={ listType } style={ align ? { textAlign: align } : null } value={ content } + focus={ focus } + onFocus={ setFocus } className="blocks-list" /> ); }, diff --git a/blocks/library/quote/index.js b/blocks/library/quote/index.js index a88a47bdf5cbb3..7d661d08032230 100644 --- a/blocks/library/quote/index.js +++ b/blocks/library/quote/index.js @@ -20,7 +20,7 @@ registerBlock( 'core/quote', { citation: html( 'footer' ) }, - edit( { attributes, setAttributes } ) { + edit( { attributes, setAttributes, focus, setFocus } ) { const { value, citation } = attributes; return ( @@ -31,16 +31,24 @@ registerBlock( 'core/quote', { ( paragraphs ) => setAttributes( { value: fromParagraphsToValue( paragraphs ) } ) - } /> - + } + focus={ focus && focus.editable === 'value' ? focus : null } + onFocus={ () => setFocus( { editable: 'value' } ) } + /> + { ( citation || !! focus ) && + + } ); }, diff --git a/blocks/library/text/index.js b/blocks/library/text/index.js index 540c8c01d232a9..007e7f4a099fe8 100644 --- a/blocks/library/text/index.js +++ b/blocks/library/text/index.js @@ -47,7 +47,7 @@ registerBlock( 'core/text', { } ], - edit( { attributes, setAttributes, insertBlockAfter } ) { + edit( { attributes, setAttributes, insertBlockAfter, focus, setFocus } ) { const { content, align } = attributes; return ( @@ -56,6 +56,8 @@ registerBlock( 'core/text', { onChange={ ( paragraphs ) => setAttributes( { content: fromParagraphsToValue( paragraphs ) } ) } + focus={ focus } + onFocus={ setFocus } style={ align ? { textAlign: align } : null } onSplit={ ( before, after ) => { setAttributes( { content: fromParagraphsToValue( before ) } ); diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 5284eb483644ef..703ca8bb70ff6f 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -17,6 +17,7 @@ class VisualEditorBlock extends wp.element.Component { this.bindBlockNode = this.bindBlockNode.bind( this ); this.setAttributes = this.setAttributes.bind( this ); this.maybeDeselect = this.maybeDeselect.bind( this ); + this.maybeHover = this.maybeHover.bind( this ); this.previousOffset = null; } @@ -44,6 +45,13 @@ class VisualEditorBlock extends wp.element.Component { } ); } + maybeHover() { + const { isTyping, isHovered, onHover } = this.props; + if ( isTyping && ! isHovered ) { + onHover(); + } + } + maybeDeselect( event ) { // Annoyingly React does not support focusOut and we're forced to check // related target to ensure it's not a child when blur fires. @@ -75,13 +83,13 @@ class VisualEditorBlock extends wp.element.Component { return null; } - const { isHovered, isSelected } = this.props; + const { isHovered, isSelected, isTyping, focus } = this.props; const className = classnames( 'editor-visual-editor__block', { - 'is-selected': isSelected, + 'is-selected': isSelected && ! isTyping, 'is-hovered': isHovered } ); - const { onSelect, onDeselect, onMouseEnter, onMouseLeave, onInsertAfter } = this.props; + const { onSelect, onStartTyping, onHover, onMouseLeave, onFocus, onInsertAfter } = this.props; // Disable reason: Each block can receive focus but must be able to contain // block children. Tab keyboard navigation enabled by tabIndex assignment. @@ -93,15 +101,16 @@ class VisualEditorBlock extends wp.element.Component { tabIndex="0" onFocus={ onSelect } onBlur={ this.maybeDeselect } - onKeyDown={ onDeselect } - onMouseEnter={ onMouseEnter } + onKeyDown={ onStartTyping } + onMouseEnter={ onHover } + onMouseMove={ this.maybeHover } onMouseLeave={ onMouseLeave } className={ className } > - { ( isSelected || isHovered ) && } + { ( ( isSelected && ! isTyping ) || isHovered ) && }
    - { isSelected && } - { isSelected && settings.controls ? ( + { isSelected && ! isTyping && } + { isSelected && ! isTyping && settings.controls ? ( ( { ...control, @@ -111,10 +120,11 @@ class VisualEditorBlock extends wp.element.Component { ) : null }
    ); @@ -126,8 +136,10 @@ export default connect( ( state, ownProps ) => ( { order: state.blocks.order.indexOf( ownProps.uid ), block: state.blocks.byUid[ ownProps.uid ], - isSelected: state.selectedBlock === ownProps.uid, - isHovered: state.hoveredBlock === ownProps.uid + isSelected: state.selectedBlock.uid === ownProps.uid, + isHovered: state.hoveredBlock === ownProps.uid, + focus: state.selectedBlock.uid === ownProps.uid ? state.selectedBlock.focus : null, + isTyping: state.selectedBlock.uid === ownProps.uid ? state.selectedBlock.typing : false, } ), ( dispatch, ownProps ) => ( { onChange( updates ) { @@ -151,7 +163,13 @@ export default connect( uid: ownProps.uid } ); }, - onMouseEnter() { + onStartTyping() { + dispatch( { + type: 'START_TYPING', + uid: ownProps.uid + } ); + }, + onHover() { dispatch( { type: 'TOGGLE_BLOCK_HOVERED', hovered: true, @@ -165,12 +183,21 @@ export default connect( uid: ownProps.uid } ); }, + onInsertAfter( block ) { dispatch( { type: 'INSERT_BLOCK', after: ownProps.uid, block } ); + }, + + onFocus( config ) { + dispatch( { + type: 'UPDATE_FOCUS', + uid: ownProps.uid, + config + } ); } } ) )( VisualEditorBlock ); diff --git a/editor/state.js b/editor/state.js index 6a1dc174172bbc..f9e35785522ca4 100644 --- a/editor/state.js +++ b/editor/state.js @@ -100,17 +100,49 @@ export const blocks = combineUndoableReducers( { * @param {Object} action Dispatched action * @return {Object} Updated state */ -export function selectedBlock( state = null, action ) { +export function selectedBlock( state = {}, action ) { switch ( action.type ) { case 'TOGGLE_BLOCK_SELECTED': - return action.selected ? action.uid : null; + if ( ! action.selected ) { + return state.uid === action.uid ? {} : state; + } + return action.uid === state.uid + ? state + : { uid: action.uid, typing: false, focus: {} }; case 'MOVE_BLOCK_UP': case 'MOVE_BLOCK_DOWN': - return action.uid; + return action.uid === state.uid + ? state + : { uid: action.uid, typing: false, focus: {} }; case 'INSERT_BLOCK': - return action.block.uid; + return { + uid: action.block.uid, + typing: false, + focus: {} + }; + + case 'UPDATE_FOCUS': + return { + uid: action.uid, + typing: state.uid === action.uid ? state.typing : false, + focus: action.config || {} + }; + + case 'START_TYPING': + if ( action.uid !== state.uid ) { + return { + uid: action.uid, + typing: true, + focus: {} + }; + } + + return { + ...state, + typing: true + }; } return state; @@ -133,6 +165,8 @@ export function hoveredBlock( state = null, action ) { return null; } break; + case 'START_TYPING': + return null; } return state; diff --git a/editor/test/state.js b/editor/test/state.js index 24b04c37abb835..5c7e89c25ef1f6 100644 --- a/editor/test/state.js +++ b/editor/test/state.js @@ -3,6 +3,7 @@ */ import { expect } from 'chai'; import { values } from 'lodash'; +import deepFreeze from 'deep-freeze'; /** * Internal dependencies @@ -226,7 +227,40 @@ describe( 'state', () => { selected: true } ); - expect( state ).to.equal( 'kumquat' ); + expect( state ).to.eql( { uid: 'kumquat', typing: false, focus: {} } ); + } ); + + it( 'should not update the state if already selected', () => { + const original = deepFreeze( { uid: 'kumquat', typing: true, focus: {} } ); + const state = selectedBlock( original, { + type: 'TOGGLE_BLOCK_SELECTED', + uid: 'kumquat', + selected: true + } ); + + expect( state ).to.equal( original ); + } ); + + it( 'should unselect the block if currently selected', () => { + const original = deepFreeze( { uid: 'kumquat', typing: true, focus: {} } ); + const state = selectedBlock( original, { + type: 'TOGGLE_BLOCK_SELECTED', + uid: 'kumquat', + selected: false + } ); + + expect( state ).to.eql( {} ); + } ); + + it( 'should not unselect the block if another block is selected', () => { + const original = deepFreeze( { uid: 'loquat', typing: true, focus: {} } ); + const state = selectedBlock( original, { + type: 'TOGGLE_BLOCK_SELECTED', + uid: 'kumquat', + selected: false + } ); + + expect( state ).to.equal( original ); } ); it( 'should return with inserted block', () => { @@ -238,7 +272,7 @@ describe( 'state', () => { } } ); - expect( state ).to.equal( 'ribs' ); + expect( state ).to.eql( { uid: 'ribs', typing: false, focus: {} } ); } ); it( 'should return with block moved up', () => { @@ -247,7 +281,7 @@ describe( 'state', () => { uid: 'ribs' } ); - expect( state ).to.equal( 'ribs' ); + expect( state ).to.eql( { uid: 'ribs', typing: false, focus: {} } ); } ); it( 'should return with block moved down', () => { @@ -256,7 +290,57 @@ describe( 'state', () => { uid: 'chicken' } ); - expect( state ).to.equal( 'chicken' ); + expect( state ).to.eql( { uid: 'chicken', typing: false, focus: {} } ); + } ); + + it( 'should not update the state if the block moved is already selected', () => { + const original = deepFreeze( { uid: 'ribs', typing: true, focus: {} } ); + const state = selectedBlock( original, { + type: 'MOVE_BLOCK_UP', + uid: 'ribs' + } ); + + expect( state ).to.equal( original ); + } ); + + it( 'should update the focus and selects the block', () => { + const state = selectedBlock( undefined, { + type: 'UPDATE_FOCUS', + uid: 'chicken', + config: { editable: 'citation' } + } ); + + expect( state ).to.eql( { uid: 'chicken', typing: false, focus: { editable: 'citation' } } ); + } ); + + it( 'should update the focus and merge the existing state', () => { + const original = deepFreeze( { uid: 'ribs', typing: true, focus: {} } ); + const state = selectedBlock( original, { + type: 'UPDATE_FOCUS', + uid: 'ribs', + config: { editable: 'citation' } + } ); + + expect( state ).to.eql( { uid: 'ribs', typing: true, focus: { editable: 'citation' } } ); + } ); + + it( 'should set the typing flag and selects the block', () => { + const state = selectedBlock( undefined, { + type: 'START_TYPING', + uid: 'chicken' + } ); + + expect( state ).to.eql( { uid: 'chicken', typing: true, focus: {} } ); + } ); + + it( 'should set the typing flag and merge the existing state', () => { + const original = deepFreeze( { uid: 'ribs', typing: false, focus: { editable: 'citation' } } ); + const state = selectedBlock( original, { + type: 'START_TYPING', + uid: 'ribs' + } ); + + expect( state ).to.eql( { uid: 'ribs', typing: true, focus: { editable: 'citation' } } ); } ); it( 'should insert after the specified block uid', () => {