From 54273b1ce3371226fc0136e212150da6ce77c58b Mon Sep 17 00:00:00 2001 From: Nicola Heald Date: Tue, 27 Jun 2017 16:53:38 +0100 Subject: [PATCH] Recently used blocks in the inserter menu --- blocks/api/categories.js | 1 + editor/inserter/menu.js | 132 ++++++++++++++++++++++++++++++--------- 2 files changed, 104 insertions(+), 29 deletions(-) diff --git a/blocks/api/categories.js b/blocks/api/categories.js index 2ca1fbbea3b8bc..ab3a8a5dd57fbb 100644 --- a/blocks/api/categories.js +++ b/blocks/api/categories.js @@ -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' ) }, diff --git a/editor/inserter/menu.js b/editor/inserter/menu.js index d9e939f18902f0..96e92ad81b8c01 100644 --- a/editor/inserter/menu.js +++ b/editor/inserter/menu.js @@ -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'; /** @@ -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 ); @@ -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() { @@ -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 ); }; } @@ -83,33 +103,80 @@ 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 ) { @@ -117,7 +184,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 ); @@ -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 ); @@ -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. @@ -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. @@ -215,7 +284,6 @@ class InserterMenu extends Component { this.setState( { currentFocus: refName, } ); - // Focus the DOM node. this.nodes[ refName ].focus(); } @@ -247,21 +315,27 @@ class InserterMenu extends Component { tabIndex="0" aria-labelledby={ `editor-inserter__separator-${ category.slug }-${ instanceId }` } > - { visibleBlocksByCategory[ category.slug ].map( ( block ) => ( - - ) ) } + { 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 ( + + ); + } ) } ) )