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

Recently used blocks in the inserter menu #1506

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions blocks/api/categories.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { __ } from 'i18n';
* @var {Array} categories
*/
const categories = [
{ slug: 'recent', title: __( 'Recently Used' ) },
{ slug: 'common', title: __( 'Common Blocks' ) },
{ slug: 'formatting', title: __( 'Formatting' ) },
{ slug: 'layout', title: __( 'Layout Blocks' ) },
Expand Down
132 changes: 103 additions & 29 deletions editor/inserter/menu.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { flow, groupBy, sortBy, findIndex, filter } from 'lodash';
import { flow, groupBy, sortBy, findIndex, filter, includes } from 'lodash';
import { connect } from 'react-redux';

/**
Expand All @@ -19,6 +19,9 @@ import { getCategories, getBlockTypes } from 'blocks';
import './style.scss';
import { showInsertionPoint, hideInsertionPoint } from '../actions';

const recentlyUsed = [];
const recentlyUsedLimit = 4;

class InserterMenu extends Component {
constructor() {
super( ...arguments );
Expand All @@ -33,6 +36,9 @@ class InserterMenu extends Component {
this.onKeyDown = this.onKeyDown.bind( this );
this.getVisibleBlocks = this.getVisibleBlocks.bind( this );
this.sortBlocksByCategory = this.sortBlocksByCategory.bind( this );
this.addRecentlyUsedBlocks = this.addRecentlyUsedBlocks.bind( this );
this.prependRecentlyUsedBlocks = this.prependRecentlyUsedBlocks.bind( this );
this.getRecentlyUsedBlocks = this.getRecentlyUsedBlocks.bind( this );
}

componentDidMount() {
Expand All @@ -57,13 +63,27 @@ class InserterMenu extends Component {
} );
}

selectBlock( name ) {
recordBlockUse( name ) {
if ( includes( recentlyUsed, name ) ) {
return;
}
recentlyUsed.unshift( name );
if ( recentlyUsed.length > recentlyUsedLimit ) {
recentlyUsed.pop();
}
}

selectBlock( blockKey ) {
return () => {
// strip off the '_{ category }' part of the block reference key, so
// other hooks work properly
const name = blockKey.replace( /_[a-zA-Z]+$/, '' );
this.props.onSelect( name );
this.setState( {
filterValue: '',
currentFocus: null,
} );
this.recordBlockUse( name );
};
}

Expand All @@ -83,41 +103,88 @@ class InserterMenu extends Component {
return groupBy( blockTypes, ( blockType ) => blockType.category );
}

getRecentlyUsedBlocks() {
if ( 0 === recentlyUsed.length ) {
return [];
}
const getRecentIndex = ( item ) => {
return findIndex( recentlyUsed, ( blockName ) => blockName === item.name );
};
return sortBy( filter( getBlockTypes(), ( block ) => includes( recentlyUsed, block.name ) ), getRecentIndex );
}

prependRecentlyUsedBlocks( blockTypes ) {
return [ ...this.getRecentlyUsedBlocks(), ...blockTypes ];
}

addRecentlyUsedBlocks( blockTypes ) {
if ( recentlyUsed.length > 0 ) {
blockTypes.recent = this.getRecentlyUsedBlocks();
}
return blockTypes;
}

getVisibleBlocksByCategory( blockTypes ) {
return flow(
this.getVisibleBlocks,
this.sortBlocksByCategory,
this.groupByCategory
this.groupByCategory,
this.addRecentlyUsedBlocks
)( blockTypes );
}

getBlockRefName( blockList, blockIndex ) {
const refNameBase = blockList[ blockIndex ].name + '_';

if ( 'search_' === refNameBase ) {
return 'search';
}

// blocks have a different reference name depending on where they are in the list,
// because blocks can be duplicated in the 'recent' category too,
// so this assigns the _recent suffix if they are at the top
// of the list, inside the bounds of the recently used category
if ( blockIndex < recentlyUsed.length ) {
return refNameBase + 'recent';
}

return refNameBase + blockList[ blockIndex ].category;
}

findByIncrement( blockTypes, increment = 1 ) {
// Add on a fake search block to the list to cycle through.
const list = blockTypes.concat( { name: 'search' } );

const currentIndex = findIndex( list, ( blockType ) => this.state.currentFocus === blockType.name );
let currentIndex = 0;
list.forEach( ( block, blockIndex ) => {
const refName = this.getBlockRefName( list, blockIndex );
if ( refName === this.state.currentFocus ) {
currentIndex = blockIndex;
}
} );

const nextIndex = currentIndex + increment;
const highestIndex = list.length - 1;
const lowestIndex = 0;
let nextRef;

if ( nextIndex > highestIndex ) {
return list[ lowestIndex ].name;
nextRef = this.getBlockRefName( list, lowestIndex );
} else if ( nextIndex < lowestIndex ) {
nextRef = this.getBlockRefName( list, highestIndex );
} else {
nextRef = this.getBlockRefName( list, nextIndex );
}

if ( nextIndex < lowestIndex ) {
return list[ highestIndex ].name;
}

// Return the name of the next block type.
return list[ nextIndex ].name;
return nextRef;
}

findNext( blockTypes ) {
/**
* null is the initial state value and triggers start at beginning.
*/
if ( null === this.state.currentFocus ) {
return blockTypes[ 0 ].name;
return this.getBlockRefName( blockTypes, 0 );
}

return this.findByIncrement( blockTypes, 1 );
Expand All @@ -128,7 +195,7 @@ class InserterMenu extends Component {
* null is the initial state value and triggers start at beginning.
*/
if ( null === this.state.currentFocus ) {
return blockTypes[ 0 ].name;
return this.getBlockRefName( blockTypes, 0 );
}

return this.findByIncrement( blockTypes, -1 );
Expand All @@ -138,6 +205,7 @@ class InserterMenu extends Component {
const sortedByCategory = flow(
this.getVisibleBlocks,
this.sortBlocksByCategory,
this.prependRecentlyUsedBlocks
)( getBlockTypes() );

// If the block list is empty return early.
Expand All @@ -153,6 +221,7 @@ class InserterMenu extends Component {
const sortedByCategory = flow(
this.getVisibleBlocks,
this.sortBlocksByCategory,
this.prependRecentlyUsedBlocks
)( getBlockTypes() );

// If the block list is empty return early.
Expand Down Expand Up @@ -215,7 +284,6 @@ class InserterMenu extends Component {
this.setState( {
currentFocus: refName,
} );

// Focus the DOM node.
this.nodes[ refName ].focus();
}
Expand Down Expand Up @@ -247,21 +315,27 @@ class InserterMenu extends Component {
tabIndex="0"
aria-labelledby={ `editor-inserter__separator-${ category.slug }-${ instanceId }` }
>
{ visibleBlocksByCategory[ category.slug ].map( ( block ) => (
<button
role="menuitem"
key={ block.name }
className="editor-inserter__block"
onClick={ this.selectBlock( block.name ) }
ref={ this.bindReferenceNode( block.name ) }
tabIndex="-1"
onMouseEnter={ this.props.showInsertionPoint }
onMouseLeave={ this.props.hideInsertionPoint }
>
<Dashicon icon={ block.icon } />
{ block.title }
</button>
) ) }
{ visibleBlocksByCategory[ category.slug ].map( ( block ) => {
// because blocks can be in multiple categories (e.g. 'common' and 'recent')
// we construct a key using the current category name
let blockKey = block.name + '_';
blockKey += 'recent' === category.slug ? 'recent' : category.slug;
return (
<button
role="menuitem"
key={ blockKey }
className="editor-inserter__block"
onClick={ this.selectBlock( blockKey ) }
ref={ this.bindReferenceNode( blockKey ) }
tabIndex="-1"
onMouseEnter={ this.props.showInsertionPoint }
onMouseLeave={ this.props.hideInsertionPoint }
>
<Dashicon icon={ block.icon } />
{ block.title }
</button>
);
} ) }
</div>
</div>
) )
Expand Down