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

Blocks: Match blocks in the inserter using keywords from patterns #19243

Merged
merged 10 commits into from
Jan 28, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function BlockPatternPicker( {
iconSize={ 48 }
onClick={ () => onSelect( pattern ) }
className="block-editor-block-pattern-picker__pattern"
label={ pattern.label }
label={ pattern.title }
/>
</li>
) ) }
Expand Down
70 changes: 52 additions & 18 deletions packages/block-editor/src/components/block-types-list/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,65 @@ import { getBlockMenuDefaultClassName } from '@wordpress/blocks';
*/
import InserterListItem from '../inserter-list-item';

function BlockTypesList( { items, onSelect, onHover = () => {}, children } ) {
function BlockTypesList( { items = [], onSelect, onHover = () => {}, children } ) {
const normalizedItems = items.reduce( ( result, item ) => {
const { patterns = [] } = item;
const hasDefaultPattern = patterns.some( ( { isDefault } ) => isDefault );

// If there is no default inserter pattern provided,
// then default block type is displayed.
if ( ! hasDefaultPattern ) {
result.push( item );
}

if ( patterns.length ) {
result = result.concat( patterns.map( ( pattern ) => {
return {
...item,
id: `${ item.id }-${ pattern.name }`,
icon: pattern.icon || item.icon,
title: pattern.title || item.title,
description: pattern.description || item.description,
// If `example` is explicitly undefined for the pattern, the preview will not be shown.
example: pattern.hasOwnProperty( 'example' ) ? pattern.example : item.example,
initialAttributes: {
...item.initialAttributes,
...pattern.attributes,
},
innerBlocks: pattern.innerBlocks,
};
} ) );
}

return result;
}, [] );

return (
/*
* Disable reason: The `list` ARIA role is redundant but
* Safari+VoiceOver won't announce the list otherwise.
*/
/* eslint-disable jsx-a11y/no-redundant-roles */
<ul role="list" className="block-editor-block-types-list">
{ items && items.map( ( item ) =>
<InserterListItem
key={ item.id }
className={ getBlockMenuDefaultClassName( item.id ) }
icon={ item.icon }
onClick={ () => {
onSelect( item );
onHover( null );
} }
onFocus={ () => onHover( item ) }
onMouseEnter={ () => onHover( item ) }
onMouseLeave={ () => onHover( null ) }
onBlur={ () => onHover( null ) }
isDisabled={ item.isDisabled }
title={ item.title }
/>
) }
{ normalizedItems.map( ( item ) => {
return (
<InserterListItem
key={ item.id }
className={ getBlockMenuDefaultClassName( item.id ) }
icon={ item.icon }
onClick={ () => {
onSelect( item );
onHover( null );
} }
onFocus={ () => onHover( item ) }
onMouseEnter={ () => onHover( item ) }
onMouseLeave={ () => onHover( null ) }
onBlur={ () => onHover( null ) }
isDisabled={ item.isDisabled }
title={ item.title }
/>
);
} ) }
{ children }
</ul>
/* eslint-enable jsx-a11y/no-redundant-roles */
Expand Down
10 changes: 8 additions & 2 deletions packages/block-editor/src/components/inserter/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { get } from 'lodash';
import { size } from 'lodash';
/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -140,12 +140,18 @@ export default compose( [
hasInserterItems,
__experimentalGetAllowedBlocks,
} = select( 'core/block-editor' );
const {
__experimentalGetBlockPatterns: getBlockPatterns,
} = select( 'core/blocks' );

rootClientId = rootClientId || getBlockRootClientId( clientId ) || undefined;

const allowedBlocks = __experimentalGetAllowedBlocks( rootClientId );

const hasSingleBlockType = allowedBlocks && ( get( allowedBlocks, [ 'length' ], 0 ) === 1 );
const hasSingleBlockType = (
size( allowedBlocks ) === 1 &&
size( getBlockPatterns( allowedBlocks[ 0 ].name, 'inserter' ) ) === 0
);

let allowedBlockType = false;
if ( hasSingleBlockType ) {
Expand Down
27 changes: 23 additions & 4 deletions packages/block-editor/src/components/inserter/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ const stopKeyPropagation = ( event ) => event.stopPropagation();

const getBlockNamespace = ( item ) => item.name.split( '/' )[ 0 ];

// Copied over from the Columns block. It seems like it should become part of public API.
const createBlocksFromInnerBlocksTemplate = ( innerBlocksTemplate ) => {
return map(
innerBlocksTemplate,
( [ name, attributes, innerBlocks = [] ] ) =>
createBlock( name, attributes, createBlocksFromInnerBlocksTemplate( innerBlocks ) )
);
};

export class InserterMenu extends Component {
constructor() {
super( ...arguments );
Expand Down Expand Up @@ -383,7 +392,7 @@ export class InserterMenu extends Component {
{ hoveredItem && (
<>
{ ! isReusableBlock( hoveredItem ) && (
<BlockCard blockType={ hoveredItemBlockType } />
<BlockCard blockType={ hoveredItem } />
) }
<div className="block-editor-inserter__preview">
{ ( isReusableBlock( hoveredItem ) || hoveredItemBlockType.example ) ? (
Expand All @@ -393,7 +402,13 @@ export class InserterMenu extends Component {
viewportWidth={ 500 }
blocks={
hoveredItemBlockType.example ?
getBlockFromExample( hoveredItem.name, hoveredItemBlockType.example ) :
getBlockFromExample( hoveredItem.name, {
attributes: {
...hoveredItemBlockType.example.attributes,
...hoveredItem.initialAttributes,
},
innerBlocks: hoveredItemBlockType.example.innerBlocks,
} ) :
createBlock( hoveredItem.name, hoveredItem.initialAttributes )
}
/>
Expand Down Expand Up @@ -534,9 +549,13 @@ export default compose(
onSelect,
__experimentalSelectBlockOnInsert: selectBlockOnInsert,
} = ownProps;
const { name, title, initialAttributes } = item;
const { name, title, initialAttributes, innerBlocks } = item;
const selectedBlock = getSelectedBlock();
const insertedBlock = createBlock( name, initialAttributes );
const insertedBlock = createBlock(
name,
initialAttributes,
createBlocksFromInnerBlocksTemplate( innerBlocks )
);

if ( ! isAppender && selectedBlock && isUnmodifiedDefaultBlock( selectedBlock ) ) {
replaceBlocks( selectedBlock.clientId, insertedBlock );
Expand Down
40 changes: 36 additions & 4 deletions packages/block-editor/src/components/inserter/search-items.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
differenceWith,
find,
get,
intersectionWith,
isEmpty,
words,
} from 'lodash';

Expand Down Expand Up @@ -52,15 +54,15 @@ const removeMatchingTerms = ( unmatchedTerms, unprocessedTerms ) => {
* @return {Array} Filtered item list.
*/
export const searchItems = ( items, categories, collections, searchTerm ) => {
const normalizedTerms = normalizeSearchTerm( searchTerm );
const normalizedSearchTerms = normalizeSearchTerm( searchTerm );

if ( normalizedTerms.length === 0 ) {
if ( normalizedSearchTerms.length === 0 ) {
return items;
}

return items.filter( ( { name, title, category, keywords = [] } ) => {
return items.filter( ( { name, title, category, keywords = [], patterns = [] } ) => {
let unmatchedTerms = removeMatchingTerms(
normalizedTerms,
normalizedSearchTerms,
title
);

Expand Down Expand Up @@ -90,6 +92,36 @@ export const searchItems = ( items, categories, collections, searchTerm ) => {
);
}

if ( unmatchedTerms.length === 0 ) {
return true;
}

unmatchedTerms = removeMatchingTerms(
unmatchedTerms,
patterns.map( ( pattern ) => pattern.title ).join( ' ' ),
);

return unmatchedTerms.length === 0;
} ).map( ( item ) => {
if ( isEmpty( item.patterns ) ) {
return item;
}

const matchedPatterns = item.patterns.filter( ( pattern ) => {
return intersectionWith(
normalizedSearchTerms,
normalizeSearchTerm( pattern.title ),
( termToMatch, labelTerm ) => labelTerm.includes( termToMatch )
).length > 0;
} );
// When no partterns matched, fallback to all patterns.
if ( isEmpty( matchedPatterns ) ) {
return item;
}

return {
...item,
patterns: matchedPatterns,
};
} );
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ export const textItem = {
utility: 1,
};

export const withPatternsItem = {
id: 'core/block-with-patterns',
name: 'core/block-with-patterns',
initialAttributes: {},
title: 'With Patterns',
category: 'widgets',
isDisabled: false,
utility: 0,
patterns: [
{
name: 'pattern-one',
title: 'Pattern One',
},
{
name: 'pattern-two',
title: 'Pattern Two',
},
{
name: 'pattern-three',
title: 'Pattern Three',
},
],
};

export const advancedTextItem = {
id: 'core/advanced-text-block',
name: 'core/advanced-text-block',
Expand Down Expand Up @@ -87,6 +111,7 @@ export const reusableItem = {

export default [
textItem,
withPatternsItem,
advancedTextItem,
someOtherItem,
moreItem,
Expand Down
16 changes: 16 additions & 0 deletions packages/block-editor/src/components/inserter/test/search-items.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,20 @@ describe( 'searchItems', () => {
[ youtubeItem ]
);
} );

it( 'should match words using also patterns and return all matched patterns', () => {
const filteredItems = searchItems( items, categories, collections, 'pattern' );

expect( filteredItems ).toHaveLength( 1 );
expect( filteredItems[ 0 ].patterns ).toHaveLength( 3 );
} );

it( 'should match words using also patterns and filter out unmatched patterns', () => {
const filteredItems = searchItems( items, categories, collections, 'patterns two three' );

expect( filteredItems ).toHaveLength( 1 );
expect( filteredItems[ 0 ].patterns ).toHaveLength( 2 );
expect( filteredItems[ 0 ].patterns[ 0 ].title ).toBe( 'Pattern Two' );
expect( filteredItems[ 0 ].patterns[ 1 ].title ).toBe( 'Pattern Three' );
} );
} );
6 changes: 6 additions & 0 deletions packages/block-editor/src/store/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1255,15 +1255,21 @@ export const getInserterItems = createSelector(

const isContextual = isArray( blockType.parent );
const { time, count = 0 } = getInsertUsage( state, id ) || {};
const inserterPatterns = blockType.patterns.filter(
( { scope } ) => ! scope || scope.includes( 'inserter' )
);

return {
id,
name: blockType.name,
initialAttributes: {},
title: blockType.title,
description: blockType.description,
icon: blockType.icon,
category: blockType.category,
keywords: blockType.keywords,
patterns: inserterPatterns,
example: blockType.example,
isDisabled,
utility: calculateUtility( blockType.category, count, isContextual ),
frecency: calculateFrecency( time, count ),
Expand Down
1 change: 1 addition & 0 deletions packages/block-editor/src/store/test/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -2015,6 +2015,7 @@ describe( 'selectors', () => {
},
category: 'formatting',
keywords: [ 'testing' ],
patterns: [],
isDisabled: false,
utility: 0,
frecency: 0,
Expand Down
4 changes: 2 additions & 2 deletions packages/block-library/src/columns/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ const ColumnsEdit = ( props ) => {

return {
blockType: getBlockType( name ),
defaultPattern: __experimentalGetDefaultBlockPattern( name ),
defaultPattern: __experimentalGetDefaultBlockPattern( name, 'block' ),
hasInnerBlocks: select( 'core/block-editor' ).getBlocks( clientId ).length > 0,
patterns: __experimentalGetBlockPatterns( name ),
patterns: __experimentalGetBlockPatterns( name, 'block' ),
};
}, [ clientId, name ] );

Expand Down
Loading