diff --git a/editor/inserter/menu.js b/editor/inserter/menu.js
index 7a91f440de8f0e..b847eca5e10537 100644
--- a/editor/inserter/menu.js
+++ b/editor/inserter/menu.js
@@ -18,6 +18,7 @@ import { getCategories, getBlockTypes } from 'blocks';
*/
import './style.scss';
import { showInsertionPoint, hideInsertionPoint } from '../actions';
+import { getRecentlyUsedBlocks } from '../selectors';
class InserterMenu extends Component {
constructor() {
@@ -29,11 +30,12 @@ class InserterMenu extends Component {
tab: 'recent',
};
this.filter = this.filter.bind( this );
- this.isShownBlock = this.isShownBlock.bind( this );
this.setSearchFocus = this.setSearchFocus.bind( this );
this.onKeyDown = this.onKeyDown.bind( this );
- this.getVisibleBlocks = this.getVisibleBlocks.bind( this );
- this.sortBlocksByCategory = this.sortBlocksByCategory.bind( this );
+ this.searchBlocks = this.searchBlocks.bind( this );
+ this.getBlocksForCurrentTab = this.getBlocksForCurrentTab.bind( this );
+ this.sortBlocks = this.sortBlocks.bind( this );
+ this.addRecentBlocks = this.addRecentBlocks.bind( this );
}
componentDidMount() {
@@ -44,10 +46,6 @@ class InserterMenu extends Component {
document.removeEventListener( 'keydown', this.onKeyDown );
}
- isShownBlock( block ) {
- return block.title.toLowerCase().indexOf( this.state.filterValue.toLowerCase() ) !== -1;
- }
-
bindReferenceNode( nodeName ) {
return ( node ) => this.nodes[ nodeName ] = node;
}
@@ -68,11 +66,31 @@ class InserterMenu extends Component {
};
}
- getVisibleBlocks( blockTypes ) {
- return filter( blockTypes, this.isShownBlock );
+ searchBlocks( blockTypes ) {
+ const matchesSearch = ( block ) => block.title.toLowerCase().indexOf( this.state.filterValue.toLowerCase() ) !== -1;
+ return filter( blockTypes, matchesSearch );
+ }
+
+ getBlocksForCurrentTab() {
+ // if we're searching, use everything, otherwise just get the blocks visible in this tab
+ if ( this.state.filterValue ) {
+ return getBlockTypes();
+ }
+ switch ( this.state.tab ) {
+ case 'recent':
+ return this.props.recentlyUsedBlocks;
+ case 'blocks':
+ return filter( getBlockTypes(), ( block ) => block.category !== 'embed' );
+ case 'embeds':
+ return filter( getBlockTypes(), ( block ) => block.category === 'embed' );
+ }
}
- sortBlocksByCategory( blockTypes ) {
+ sortBlocks( blockTypes ) {
+ if ( 'recent' === this.state.tab && ! this.state.filterValue ) {
+ return blockTypes;
+ }
+
const getCategoryIndex = ( item ) => {
return findIndex( getCategories(), ( category ) => category.slug === item.category );
};
@@ -80,15 +98,21 @@ class InserterMenu extends Component {
return sortBy( blockTypes, getCategoryIndex );
}
+ addRecentBlocks( blocksByCategory ) {
+ blocksByCategory.recent = this.props.recentlyUsedBlocks;
+ return blocksByCategory;
+ }
+
groupByCategory( blockTypes ) {
return groupBy( blockTypes, ( blockType ) => blockType.category );
}
getVisibleBlocksByCategory( blockTypes ) {
return flow(
- this.getVisibleBlocks,
- this.sortBlocksByCategory,
- this.groupByCategory
+ this.searchBlocks,
+ this.sortBlocks,
+ this.groupByCategory,
+ this.addRecentBlocks
)( blockTypes );
}
@@ -137,9 +161,9 @@ class InserterMenu extends Component {
focusNext() {
const sortedByCategory = flow(
- this.getVisibleBlocks,
- this.sortBlocksByCategory,
- )( getBlockTypes() );
+ this.searchBlocks,
+ this.sortBlocks,
+ )( this.getBlocksForCurrentTab() );
// If the block list is empty return early.
if ( ! sortedByCategory.length ) {
@@ -152,9 +176,9 @@ class InserterMenu extends Component {
focusPrevious() {
const sortedByCategory = flow(
- this.getVisibleBlocks,
- this.sortBlocksByCategory,
- )( getBlockTypes() );
+ this.searchBlocks,
+ this.sortBlocks,
+ )( this.getBlocksForCurrentTab() );
// If the block list is empty return early.
if ( ! sortedByCategory.length ) {
@@ -249,8 +273,8 @@ class InserterMenu extends Component {
render() {
const { position, instanceId } = this.props;
- const visibleBlocksByCategory = this.getVisibleBlocksByCategory( getBlockTypes() );
const isSearching = this.state.filterValue;
+ const visibleBlocksByCategory = this.getVisibleBlocksByCategory( this.getBlocksForCurrentTab() );
/* eslint-disable jsx-a11y/no-autofocus */
return (
@@ -272,19 +296,15 @@ class InserterMenu extends Component {
{ this.state.tab === 'recent' && ! isSearching &&
- { getCategories()
- .map( ( category ) => category.slug === 'common' && !! visibleBlocksByCategory[ category.slug ] && (
-
- { visibleBlocksByCategory[ category.slug ].map( ( block ) => this.getBlockItem( block ) ) }
-
- ) )
- }
+
+ { visibleBlocksByCategory.recent.map( ( block ) => this.getBlockItem( block ) ) }
+
}
{ this.state.tab === 'blocks' && ! isSearching &&
@@ -375,7 +395,11 @@ class InserterMenu extends Component {
}
const connectComponent = connect(
- undefined,
+ ( state ) => {
+ return {
+ recentlyUsedBlocks: getRecentlyUsedBlocks( state ),
+ };
+ },
{ showInsertionPoint, hideInsertionPoint }
);
diff --git a/editor/selectors.js b/editor/selectors.js
index a6c4175177ac86..6e71424290b7d9 100644
--- a/editor/selectors.js
+++ b/editor/selectors.js
@@ -5,6 +5,11 @@ import moment from 'moment';
import { first, last, get, values } from 'lodash';
import createSelector from 'rememo';
+/**
+ * WordPress dependencies
+ */
+import { getBlockType } from 'blocks';
+
/**
* Internal dependencies
*/
@@ -668,3 +673,14 @@ export function getSuggestedPostFormat( state ) {
export function getNotices( state ) {
return values( state.notices );
}
+
+/**
+ * Resolves the list of recently used block names into a list of block type settings.
+ *
+ * @param {Object} state Global application state
+ * @return {Array} List of recently used blocks
+ */
+export function getRecentlyUsedBlocks( state ) {
+ // resolves the block names in the state to the block type settings
+ return state.editor.recentlyUsedBlocks.map( blockType => getBlockType( blockType ) );
+}
diff --git a/editor/state.js b/editor/state.js
index 97c41a0226c43c..b53c47893c0f10 100644
--- a/editor/state.js
+++ b/editor/state.js
@@ -6,6 +6,11 @@ import { combineReducers, applyMiddleware, createStore } from 'redux';
import refx from 'refx';
import { reduce, keyBy, first, last, omit, without, flowRight } from 'lodash';
+/**
+ * WordPress dependencies
+ */
+import { getBlockTypes } from 'blocks';
+
/**
* Internal dependencies
*/
@@ -219,6 +224,28 @@ export const editor = combineUndoableReducers( {
return state;
},
+
+ recentlyUsedBlocks( state = [], action ) {
+ const maxRecent = 8;
+ switch ( action.type ) {
+ case 'SETUP_NEW_POST':
+ // This is where we initially populate the recently used blocks,
+ // for now this inserts blocks from the common category.
+ return getBlockTypes()
+ .filter( ( blockType ) => 'common' === blockType.category )
+ .slice( 0, maxRecent )
+ .map( ( blockType ) => blockType.name );
+ case 'INSERT_BLOCKS':
+ // This is where we record the block usage so it can show up in
+ // the recent blocks.
+ let newState = [ ...state ];
+ action.blocks.forEach( ( block ) => {
+ newState = [ block.name, ...without( newState, block.name ) ];
+ } );
+ return newState.slice( 0, maxRecent );
+ }
+ return state;
+ },
}, { resetTypes: [ 'RESET_BLOCKS' ] } );
/**
diff --git a/editor/test/state.js b/editor/test/state.js
index 9e6786ba1923e9..78bf877208a815 100644
--- a/editor/test/state.js
+++ b/editor/test/state.js
@@ -29,7 +29,11 @@ import {
describe( 'state', () => {
describe( 'editor()', () => {
beforeAll( () => {
- registerBlockType( 'core/test-block', { save: noop } );
+ registerBlockType( 'core/test-block', {
+ save: noop,
+ edit: noop,
+ category: 'common',
+ } );
} );
afterAll( () => {
@@ -78,6 +82,42 @@ describe( 'state', () => {
expect( state.blockOrder ).toEqual( [ 'chicken', 'ribs' ] );
} );
+ it( 'should record recently used blocks', () => {
+ const original = editor( undefined, {} );
+ const state = editor( original, {
+ type: 'INSERT_BLOCKS',
+ blocks: [ {
+ uid: 'bacon',
+ name: 'core-embed/twitter',
+ } ],
+ } );
+
+ expect( state.recentlyUsedBlocks[ 0 ] ).toEqual( 'core-embed/twitter' );
+
+ const twoRecentBlocks = editor( state, {
+ type: 'INSERT_BLOCKS',
+ blocks: [ {
+ uid: 'eggs',
+ name: 'core-embed/youtube',
+ } ],
+ } );
+
+ expect( twoRecentBlocks.recentlyUsedBlocks[ 0 ] ).toEqual( 'core-embed/youtube' );
+ expect( twoRecentBlocks.recentlyUsedBlocks[ 1 ] ).toEqual( 'core-embed/twitter' );
+ } );
+
+ it( 'should populate recently used blocks with the common category', () => {
+ const initial = editor( undefined, {
+ type: 'SETUP_NEW_POST',
+ edits: {
+ status: 'draft',
+ title: 'post title',
+ },
+ } );
+
+ expect( initial.recentlyUsedBlocks ).toEqual( expect.arrayContaining( [ 'core/test-block', 'core/text' ] ) );
+ } );
+
it( 'should replace the block', () => {
const original = editor( undefined, {
type: 'RESET_BLOCKS',