Skip to content
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

Inserter: Add a visual placeholder at the position where the block is going to be inserted #835

Merged
merged 4 commits into from
May 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions editor/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ export function replaceBlocks( uids, blocks ) {
};
}

export function insertBlock( block, after ) {
return {
type: 'INSERT_BLOCK',
block,
after,
};
}

export function setInsertionPoint( uid ) {
return {
type: 'SET_INSERTION_POINT',
uid,
};
}

export function clearInsertionPoint() {
return {
type: 'CLEAR_INSERTION_POINT',
};
}

export function editPost( edits ) {
return {
type: 'EDIT_POST',
Expand Down
25 changes: 18 additions & 7 deletions editor/inserter/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import IconButton from 'components/icon-button';
* Internal dependencies
*/
import InserterMenu from './menu';
import { getSelectedBlock } from '../selectors';
import { insertBlock, clearInsertionPoint } from '../actions';

class Inserter extends wp.element.Component {
constructor() {
Expand Down Expand Up @@ -45,7 +47,11 @@ class Inserter extends wp.element.Component {

insertBlock( slug ) {
if ( slug ) {
this.props.onInsertBlock( slug );
const { selectedBlock, onInsertBlock } = this.props;
onInsertBlock(
slug,
selectedBlock ? selectedBlock.uid : null
);
} else if ( this.toggleNode ) {
// When menu is closed by pressing escape, restore focus to the
// original opening active element before menu closes
Expand Down Expand Up @@ -88,13 +94,18 @@ class Inserter extends wp.element.Component {
}

export default connect(
undefined,
( state ) => {
return {
selectedBlock: getSelectedBlock( state ),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that the inserter should have no knowledge of what block is selected. The inserter, in a way, only cares about inserting a block and dispatching the "show insertion point". Once we process the insert action the app would know the selected block internally and handle that aspect. What are your thoughts there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting we trigger the insertBlock action here without specifying the position where we'll insert the block (see after argument in onInsertBlock)?

I guess this is possible, but this complexifies the blocks.order reducer a lot, because we'll need to be aware of the insertedBlock sub-reducer value inside the blocks.order reducer to find the right position. Which means we can't use combineReducers utility.

I think we shouldn't introduce this complexity in the state and instead have all the required information to process the insert in the INSERT action.

};
},
( dispatch ) => ( {
onInsertBlock( slug ) {
dispatch( {
type: 'INSERT_BLOCK',
block: wp.blocks.createBlock( slug ),
} );
onInsertBlock( slug, after ) {
dispatch( clearInsertionPoint() );
dispatch( insertBlock(
wp.blocks.createBlock( slug ),
after
) );
},
} )
)( clickOutside( Inserter ) );
26 changes: 25 additions & 1 deletion editor/inserter/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/
import { flow, groupBy, sortBy, findIndex, filter } from 'lodash';
import classnames from 'classnames';
import { connect } from 'react-redux';

/**
* WordPress dependencies
Expand All @@ -13,6 +14,8 @@ import Dashicon from 'components/dashicon';
* Internal dependencies
*/
import './style.scss';
import { getSelectedBlock } from '../selectors';
import { setInsertionPoint, clearInsertionPoint } from '../actions';

class InserterMenu extends wp.element.Component {
constructor() {
Expand Down Expand Up @@ -63,6 +66,18 @@ class InserterMenu extends wp.element.Component {
};
}

hoverBlock() {
return () => {
this.props.setInsertionPoint(
this.props.selectedBlock ? this.props.selectedBlock.uid : null
);
};
}

unhoverBlock() {
return () => this.props.clearInsertionPoint();
}

getVisibleBlocks( blockTypes ) {
return filter( blockTypes, this.isShownBlock );
}
Expand Down Expand Up @@ -254,6 +269,8 @@ class InserterMenu extends wp.element.Component {
onClick={ this.selectBlock( slug ) }
ref={ this.bindReferenceNode( slug ) }
tabIndex="-1"
onMouseEnter={ this.hoverBlock() }
onMouseLeave={ this.unhoverBlock() }
>
<Dashicon icon={ icon } />
{ title }
Expand Down Expand Up @@ -284,4 +301,11 @@ class InserterMenu extends wp.element.Component {

InserterMenu.instances = 0;

export default InserterMenu;
export default connect(
( state ) => {
return {
selectedBlock: getSelectedBlock( state ),
};
},
{ setInsertionPoint, clearInsertionPoint }
)( InserterMenu );
4 changes: 2 additions & 2 deletions editor/inserter/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,13 @@ input[type=search].editor-inserter__search {
padding: 8px;
align-items: center;
cursor: pointer;
border: 1px solid transparent;
border: none;
background: none;
line-height: 20px;

&:hover,
&:focus {
border: 1px solid $dark-gray-500;
color: $blue-medium;
outline: none;
position: relative;
}
Expand Down
7 changes: 2 additions & 5 deletions editor/modes/visual-editor/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
deselectBlock,
focusBlock,
mergeBlocks,
insertBlock,
} from '../../actions';
import {
getPreviousBlock,
Expand Down Expand Up @@ -281,11 +282,7 @@ export default connect(
},

onInsertAfter( block ) {
dispatch( {
type: 'INSERT_BLOCK',
after: ownProps.uid,
block,
} );
dispatch( insertBlock( block, ownProps.uid ) );
},

onFocus( ...args ) {
Expand Down
27 changes: 20 additions & 7 deletions editor/modes/visual-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,19 @@ import './style.scss';
import Inserter from '../../inserter';
import VisualEditorBlock from './block';
import PostTitle from '../../post-title';
import { getBlockUids } from '../../selectors';
import { getBlockUids, getBlockInsertionPoint } from '../../selectors';

function VisualEditor( { blocks, clearSelectedBlock } ) {
const INSERTION_POINT_PLACEHOLDER = '[[insertion-point]]';

function VisualEditor( { blocks, insertionPoint, clearSelectedBlock } ) {
const insertionPointIndex = blocks.indexOf( insertionPoint );
const blocksWithInsertionPoint = insertionPoint
? [
...blocks.slice( 0, insertionPointIndex + 1 ),
INSERTION_POINT_PLACEHOLDER,
...blocks.slice( insertionPointIndex + 1 ),
]
: blocks;
// Disable reason: Focus transfer between blocks and key events are handled
// by focused block element. Consider unhandled click bubbling as unselect.

Expand All @@ -29,9 +39,12 @@ function VisualEditor( { blocks, clearSelectedBlock } ) {
onClick={ clearSelectedBlock }
className="editor-visual-editor">
<PostTitle />
{ blocks.map( ( uid ) => (
<VisualEditorBlock key={ uid } uid={ uid } />
) ) }
{ blocksWithInsertionPoint.map( ( uid ) => {
if ( uid === INSERTION_POINT_PLACEHOLDER ) {
return <div key={ INSERTION_POINT_PLACEHOLDER } className="editor-visual-editor__insertion-point" />;
}
return <VisualEditorBlock key={ uid } uid={ uid } />;
} ) }
<Inserter position="top right" />
</div>
);
Expand All @@ -41,8 +54,8 @@ function VisualEditor( { blocks, clearSelectedBlock } ) {
export default connect(
( state ) => ( {
blocks: getBlockUids( state ),
insertionPoint: getBlockInsertionPoint( state ),
} ),
( dispatch ) => ( {
clearSelectedBlock: () => dispatch( { type: 'CLEAR_SELECTED_BLOCK' } ),
} )
)( VisualEditor );
} ) )( VisualEditor );
14 changes: 14 additions & 0 deletions editor/modes/visual-editor/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,17 @@
.editor-visual-editor .editor-inserter {
margin: $item-spacing $item-spacing $item-spacing calc( 50% - #{ $visual-editor-max-width / 2 } ); // account for full-width trick
}

.editor-visual-editor > .editor-visual-editor__insertion-point {
position: relative;

&:before {
position: absolute;
top: 6px;
right: #{ $block-padding + $block-mover-margin };
left: 0;
height: 3px;
background: $blue-medium;
content: '';
}
}
9 changes: 9 additions & 0 deletions editor/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ export function isTypingInBlock( state, uid ) {
return state.selectedBlock.uid === uid ? state.selectedBlock.typing : false;
}

export function getBlockInsertionPoint( state ) {
if ( ! state.insertionPoint.show ) {
return null;
}
const blockToInsertAfter = state.insertionPoint.uid;

return blockToInsertAfter || last( state.editor.blockOrder );
}

export function isSavingPost( state ) {
return state.saving.requesting;
}
Expand Down
24 changes: 24 additions & 0 deletions editor/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,29 @@ export function hoveredBlock( state = null, action ) {
return state;
}

/**
* Reducer returning the block insertion point
*
* @param {Object} state Current state
* @param {Object} action Dispatched action
* @return {Object} Updated state
*/
export function insertionPoint( state = { show: false }, action ) {
switch ( action.type ) {
case 'SET_INSERTION_POINT':
return {
show: true,
uid: action.uid,
};
case 'CLEAR_INSERTION_POINT':
return {
show: false,
};
}

return state;
}

/**
* Reducer returning current editor mode, either "visual" or "text".
*
Expand Down Expand Up @@ -355,6 +378,7 @@ export function createReduxStore() {
currentPost,
selectedBlock,
hoveredBlock,
insertionPoint,
mode,
isSidebarOpened,
saving,
Expand Down
38 changes: 38 additions & 0 deletions editor/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
isBlockHovered,
getBlockFocus,
isTypingInBlock,
getBlockInsertionPoint,
isSavingPost,
didPostSaveRequestSucceed,
didPostSaveRequestFail,
Expand Down Expand Up @@ -580,6 +581,43 @@ describe( 'selectors', () => {
} );
} );

describe( 'getBlockInsertionPoint', () => {
it( 'should return the uid of the insertion point', () => {
const state = {
insertionPoint: {
show: true,
uid: 123,
},
};

expect( getBlockInsertionPoint( state ) ).to.equal( 123 );
} );

it( 'should return return the last block uid if the insertion point is null', () => {
const state = {
insertionPoint: {
show: true,
uid: null,
},
editor: {
blockOrder: [ 34, 23 ],
},
};

expect( getBlockInsertionPoint( state ) ).to.equal( 23 );
} );

it( 'should return null if the insertion point is not shown', () => {
const state = {
insertionPoint: {
show: false,
},
};

expect( getBlockInsertionPoint( state, 23 ) ).to.be.null();
} );
} );

describe( 'isSavingPost', () => {
it( 'should return true if the post is currently being saved', () => {
const state = {
Expand Down
26 changes: 26 additions & 0 deletions editor/test/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
mode,
isSidebarOpened,
saving,
insertionPoint,
createReduxStore,
} from '../state';

Expand Down Expand Up @@ -397,6 +398,30 @@ describe( 'state', () => {
} );
} );

describe( 'insertionPoint', () => {
it( 'should set the insertion point', () => {
const state = insertionPoint( {}, {
type: 'SET_INSERTION_POINT',
uid: 'kumquat',
} );

expect( state ).to.eql( {
show: true,
uid: 'kumquat',
} );
} );

it( 'should clear the insertion point', () => {
const state = insertionPoint( {}, {
type: 'CLEAR_INSERTION_POINT',
} );

expect( state ).to.eql( {
show: false,
} );
} );
} );

describe( 'selectedBlock()', () => {
it( 'should return with block uid as selected', () => {
const state = selectedBlock( undefined, {
Expand Down Expand Up @@ -675,6 +700,7 @@ describe( 'state', () => {
'mode',
'isSidebarOpened',
'saving',
'insertionPoint',
] );
} );
} );
Expand Down