diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index fa1c1f3f6baa4d..a6babfbdacfa65 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -2,8 +2,10 @@ * External dependencies */ import { connect } from 'react-redux'; +import classnames from 'classnames'; -function VisualEditorBlock( { block, onChange } ) { +function VisualEditorBlock( props ) { + const { block } = props; const settings = wp.blocks.getBlockSettings( block.blockType ); let BlockEdit; @@ -16,7 +18,7 @@ function VisualEditorBlock( { block, onChange } ) { } function onAttributesChange( attributes ) { - onChange( { + props.onChange( { attributes: { ...block.attributes, ...attributes @@ -24,16 +26,41 @@ function VisualEditorBlock( { block, onChange } ) { } ); } + const { isSelected, isHovered } = props; + const className = classnames( 'editor-visual-editor__block', { + 'is-selected': isSelected, + 'is-hovered': isHovered + } ); + + const { onSelect, onDeselect, onMouseEnter, onMouseLeave } = props; + + // Disable reason: Each block can receive focus but must be able to contain + // block children. Tab keyboard navigation enabled by tabIndex assignment. + + /* eslint-disable jsx-a11y/no-static-element-interactions */ return ( - +
+ +
); + /* eslint-enable jsx-a11y/no-static-element-interactions */ } export default connect( ( state, ownProps ) => ( { - block: state.blocks.byUid[ ownProps.uid ] + block: state.blocks.byUid[ ownProps.uid ], + isSelected: !! state.blocks.selected[ ownProps.uid ], + isHovered: !! state.blocks.hovered[ ownProps.uid ] } ), ( dispatch, ownProps ) => ( { onChange( updates ) { @@ -42,6 +69,34 @@ export default connect( uid: ownProps.uid, updates } ); + }, + onSelect() { + dispatch( { + type: 'TOGGLE_BLOCK_SELECTED', + selected: true, + uid: ownProps.uid + } ); + }, + onDeselect() { + dispatch( { + type: 'TOGGLE_BLOCK_SELECTED', + selected: false, + uid: ownProps.uid + } ); + }, + onMouseEnter() { + dispatch( { + type: 'TOGGLE_BLOCK_HOVERED', + hovered: true, + uid: ownProps.uid + } ); + }, + onMouseLeave() { + dispatch( { + type: 'TOGGLE_BLOCK_HOVERED', + hovered: false, + uid: ownProps.uid + } ); } } ) )( VisualEditorBlock ); diff --git a/editor/modes/visual-editor/style.scss b/editor/modes/visual-editor/style.scss index 4946a69686ae71..5d5d1ee39c39be 100644 --- a/editor/modes/visual-editor/style.scss +++ b/editor/modes/visual-editor/style.scss @@ -8,4 +8,23 @@ font-size: $editor-font-size; line-height: $editor-line-height; } + + p { + margin-top: 0; + margin-bottom: 0; + } +} + +.editor-visual-editor__block { + padding: 15px; + border: 2px solid transparent; + transition: 0.2s border-color; + + &.is-hovered { + border-left: 2px solid $light-gray-500; + } + + &.is-selected { + border: 2px solid $light-gray-500; + } } diff --git a/editor/state.js b/editor/state.js index 975e2dc1ad0a2c..b5cc8b7e7bf917 100644 --- a/editor/state.js +++ b/editor/state.js @@ -22,44 +22,84 @@ export function html( state = null, action ) { } /** - * Reducer returning editor blocks state, an object with keys byUid and order, - * where blocks are parsed from current HTML markup. + * Reducer returning editor blocks state, an combined reducer of keys byUid, + * order, selected, hovered, where blocks are parsed from current HTML markup. * * @param {Object} state Current state * @param {Object} action Dispatched action * @return {Object} Updated state */ -export function blocks( state, action ) { - if ( undefined === state ) { - state = { - byUid: {}, - order: [] - }; - } +export const blocks = ( () => { + const reducer = combineReducers( { + byUid( state = {}, action ) { + switch ( action.type ) { + case 'SET_HTML': + return keyBy( action.blockNodes, 'uid' ); - switch ( action.type ) { - case 'SET_HTML': - const blockNodes = wp.blocks.parse( action.html ); - return { - byUid: keyBy( blockNodes, 'uid' ), - order: blockNodes.map( ( { uid } ) => uid ) - }; + case 'UPDATE_BLOCK': + return { + ...state, + [ action.uid ]: { + ...state[ action.uid ], + ...action.updates + } + }; + } + + return state; + }, + order( state = [], action ) { + switch ( action.type ) { + case 'SET_HTML': + return action.blockNodes.map( ( { uid } ) => uid ); + } + + return state; + }, + selected( state = {}, action ) { + switch ( action.type ) { + case 'TOGGLE_BLOCK_SELECTED': + return { + ...state, + [ action.uid ]: action.selected + }; + } - case 'UPDATE_BLOCK': - return { - ...state, - byUid: { - ...state.byUid, - [ action.uid ]: { - ...state.byUid[ action.uid ], - ...action.updates + return state; + }, + hovered( state = {}, action ) { + switch ( action.type ) { + case 'TOGGLE_BLOCK_HOVERED': + return { + ...state, + [ action.uid ]: action.hovered + }; + + case 'TOGGLE_BLOCK_SELECTED': + if ( state[ action.uid ] ) { + return { + ...state, + [ action.uid ]: false + }; } - } + break; + } + + return state; + } + } ); + + return ( state, action ) => { + if ( 'SET_HTML' === action.type ) { + action = { + ...action, + blockNodes: wp.blocks.parse( action.html ) }; - } + } - return state; -} + return reducer( state, action ); + }; +} )(); /** * Reducer returning current editor mode, either "visual" or "text". diff --git a/editor/test/state.js b/editor/test/state.js index 477dcf0d4d9969..379397a654d90e 100644 --- a/editor/test/state.js +++ b/editor/test/state.js @@ -43,19 +43,23 @@ describe( 'state', () => { wp.blocks.unregisterBlock( 'core/test-block' ); } ); - it( 'should return empty byUid, order by default', () => { + it( 'should return empty byUid, order, selected, hovered by default', () => { const state = blocks( undefined, {} ); expect( state ).to.eql( { byUid: {}, - order: [] + order: [], + selected: {}, + hovered: {} } ); } ); it( 'should key set html blocks', () => { const original = deepFreeze( { byUid: {}, - order: [] + order: [], + selected: {}, + hovered: {} } ); const state = blocks( original, { type: 'SET_HTML', @@ -64,8 +68,6 @@ describe( 'state', () => { expect( Object.keys( state.byUid ) ).to.have.lengthOf( 1 ); expect( values( state.byUid )[ 0 ].blockType ).to.equal( 'core/test-block' ); - expect( state.order ).to.have.lengthOf( 1 ); - expect( state.order[ 0 ] ).to.be.a( 'string' ); } ); it( 'should return with block updates', () => { @@ -77,7 +79,9 @@ describe( 'state', () => { attributes: {} } }, - order: [ 'kumquat' ] + order: [ 'kumquat' ], + selected: {}, + hovered: {} } ); const state = blocks( original, { type: 'UPDATE_BLOCK', @@ -90,7 +94,53 @@ describe( 'state', () => { } ); expect( state.byUid.kumquat.attributes.updated ).to.be.true(); - expect( state.order[ 0 ] ).to.equal( 'kumquat' ); + } ); + + it( 'should return with block uid as hovered', () => { + const original = deepFreeze( { + byUid: { + kumquat: { + uid: 'kumquat', + blockType: 'core/test-block', + attributes: {} + } + }, + order: [ 'kumquat' ], + selected: {}, + hovered: {} + } ); + const state = blocks( original, { + type: 'TOGGLE_BLOCK_HOVERED', + uid: 'kumquat', + hovered: true + } ); + + expect( state.hovered.kumquat ).to.be.true(); + } ); + + it( 'should return with block uid as selected, clearing hover', () => { + const original = deepFreeze( { + byUid: { + kumquat: { + uid: 'kumquat', + blockType: 'core/test-block', + attributes: {} + } + }, + order: [ 'kumquat' ], + selected: {}, + hovered: { + kumquat: true + } + } ); + const state = blocks( original, { + type: 'TOGGLE_BLOCK_SELECTED', + uid: 'kumquat', + selected: true + } ); + + expect( state.hovered.kumquat ).to.be.false(); + expect( state.selected.kumquat ).to.be.true(); } ); } ); diff --git a/package.json b/package.json index fa56c0cfd5ee0b..4cb2246f253999 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "babel-core": "^6.24.0", "babel-eslint": "^7.2.0", "babel-loader": "^6.4.1", + "babel-plugin-lodash": "^3.2.11", "babel-plugin-transform-object-rest-spread": "^6.23.0", "babel-plugin-transform-react-jsx": "^6.23.0", "babel-plugin-transform-runtime": "^6.23.0", @@ -54,7 +55,7 @@ "webpack-node-externals": "^1.5.4" }, "dependencies": { - "babel-plugin-lodash": "^3.2.11", + "classnames": "^2.2.5", "hpq": "^1.1.1", "lodash": "^4.17.4", "react-redux": "^5.0.3",