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

Template Parts: Add search to replacement modal #42459

Merged
merged 12 commits into from
Jul 20, 2022
1 change: 1 addition & 0 deletions packages/base-styles/_z-index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ $z-layers: (
".block-editor-inserter__tabs .components-tab-panel__tab-content": 0, // lower scrolling content
".block-editor-inserter__tabs .components-tab-panel__tabs": 1, // higher sticky element
".block-editor-inserter__search": 1, // higher sticky element
".block-library-template-part__selection-search": 1, // higher sticky element

// These next two share a stacking context
".interface-complementary-area .components-panel" : 0, // lower scrolling content
Expand Down
108 changes: 68 additions & 40 deletions packages/block-library/src/template-part/edit/selection-modal.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { useCallback, useMemo } from '@wordpress/element';
import { useCallback, useMemo, useState } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { useDispatch } from '@wordpress/data';
Expand All @@ -11,6 +11,10 @@ import {
__experimentalBlockPatternsList as BlockPatternsList,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
SearchControl,
__experimentalHStack as HStack,
} from '@wordpress/components';

/**
* Internal dependencies
Expand All @@ -21,6 +25,7 @@ import {
useCreateTemplatePartFromBlocks,
} from './utils/hooks';
import { createTemplatePartId } from './utils/create-template-part-id';
import { searchPatterns } from './utils/search';

export default function TemplatePartSelectionModal( {
setAttributes,
Expand All @@ -29,6 +34,8 @@ export default function TemplatePartSelectionModal( {
area,
clientId,
} ) {
const [ searchValue, setSearchValue ] = useState( '' );

// When the templatePartId is undefined,
// it means the user is creating a new one from the placeholder.
const isReplacingTemplatePartContent = !! templatePartId;
Expand All @@ -37,18 +44,24 @@ export default function TemplatePartSelectionModal( {
templatePartId
);
// We can map template parts to block patters to reuse the BlockPatternsList UI
const templartPartsAsBlockPatterns = useMemo( () => {
return templateParts.map( ( templatePart ) => ( {
const filteredTemplateParts = useMemo( () => {
const partsAsPatterns = templateParts.map( ( templatePart ) => ( {
name: createTemplatePartId( templatePart.theme, templatePart.slug ),
title: templatePart.title.rendered,
blocks: parse( templatePart.content.raw ),
templatePart,
} ) );
}, [ templateParts ] );
const shownTemplateParts = useAsyncList( templartPartsAsBlockPatterns );
const { createSuccessNotice } = useDispatch( noticesStore );

return searchPatterns( partsAsPatterns, searchValue );
}, [ templateParts, searchValue ] );
const shownTemplateParts = useAsyncList( filteredTemplateParts );
const blockPatterns = useAlternativeBlockPatterns( area, clientId );
const shownBlockPatterns = useAsyncList( blockPatterns );
const filteredBlockPatterns = useMemo( () => {
return searchPatterns( blockPatterns, searchValue );
}, [ blockPatterns, searchValue ] );
const shownBlockPatterns = useAsyncList( filteredBlockPatterns );

const { createSuccessNotice } = useDispatch( noticesStore );
const { replaceInnerBlocks } = useDispatch( blockEditorStore );

const onTemplatePartSelect = useCallback( ( templatePart ) => {
Expand All @@ -75,41 +88,56 @@ export default function TemplatePartSelectionModal( {
setAttributes
);

const hasTemplateParts = !! filteredTemplateParts.length;
const hasBlockPatterns = !! filteredBlockPatterns.length;

return (
<>
<div className="block-library-template-part__selection-content">
{ !! templartPartsAsBlockPatterns.length && (
<div>
<h2>{ __( 'Existing template parts' ) }</h2>
<BlockPatternsList
blockPatterns={ templartPartsAsBlockPatterns }
shownPatterns={ shownTemplateParts }
onClickPattern={ ( pattern ) => {
onTemplatePartSelect( pattern.templatePart );
} }
/>
</div>
) }
<div className="block-library-template-part__selection-content">
<div className="block-library-template-part__selection-search">
<SearchControl
onChange={ setSearchValue }
value={ searchValue }
label={ __( 'Search for replacements' ) }
placeholder={ __( 'Search' ) }
/>
</div>
{ hasTemplateParts && (
<div>
<h2>{ __( 'Existing template parts' ) }</h2>
<BlockPatternsList
blockPatterns={ filteredTemplateParts }
shownPatterns={ shownTemplateParts }
onClickPattern={ ( pattern ) => {
onTemplatePartSelect( pattern.templatePart );
} }
/>
</div>
) }

{ !! blockPatterns.length && (
<div>
<h2>{ __( 'Patterns' ) }</h2>
<BlockPatternsList
blockPatterns={ blockPatterns }
shownPatterns={ shownBlockPatterns }
onClickPattern={ ( pattern, blocks ) => {
if ( isReplacingTemplatePartContent ) {
replaceInnerBlocks( clientId, blocks );
} else {
createFromBlocks( blocks, pattern.title );
}
{ hasBlockPatterns && (
<div>
<h2>{ __( 'Patterns' ) }</h2>
<BlockPatternsList
blockPatterns={ filteredBlockPatterns }
shownPatterns={ shownBlockPatterns }
onClickPattern={ ( pattern, blocks ) => {
if ( isReplacingTemplatePartContent ) {
replaceInnerBlocks( clientId, blocks );
} else {
createFromBlocks( blocks, pattern.title );
}

onClose();
} }
/>
</div>
) }
</div>
</>
onClose();
} }
/>
</div>
) }

{ ! hasTemplateParts && ! hasBlockPatterns && (
<HStack alignment="center">
<p>{ __( 'No results found.' ) }</p>
</HStack>
) }
</div>
);
}
76 changes: 76 additions & 0 deletions packages/block-library/src/template-part/edit/utils/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* External dependencies
*/
import removeAccents from 'remove-accents';

/**
* Sanitizes the search input string.
*
* @param {string} input The search input to normalize.
*
* @return {string} The normalized search input.
*/
function normalizeSearchInput( input = '' ) {
// Disregard diacritics.
input = removeAccents( input );

// Trim & Lowercase.
input = input.trim().toLowerCase();

return input;
}

/**
* Get the search rank for a given pattern and a specific search term.
*
* @param {Object} pattern Pattern to rank
* @param {string} searchValue Search term
* @return {number} A pattern search rank
*/
function getPatternSearchRank( pattern, searchValue ) {
const normalizedSearchValue = normalizeSearchInput( searchValue );
const normalizedTitle = normalizeSearchInput( pattern.title );

let rank = 0;

if ( normalizedSearchValue === normalizedTitle ) {
rank += 30;
} else if ( normalizedTitle.startsWith( normalizedSearchValue ) ) {
rank += 20;
} else {
const searchTerms = normalizedSearchValue.split( ' ' );
const hasMatchedTerms = searchTerms.every( ( searchTerm ) =>
normalizedTitle.includes( searchTerm )
);

// Prefer pattern with every search word in the title.
if ( hasMatchedTerms ) {
rank += 10;
}
}

return rank;
}

/**
* Filters an pattern list given a search term.
*
* @param {Array} patterns Item list
* @param {string} searchValue Search input.
*
* @return {Array} Filtered pattern list.
*/
export function searchPatterns( patterns = [], searchValue = '' ) {
if ( ! searchValue ) {
return patterns;
}

const rankedPatterns = patterns
.map( ( pattern ) => {
return [ pattern, getPatternSearchRank( pattern, searchValue ) ];
} )
.filter( ( [ , rank ] ) => rank > 0 );

rankedPatterns.sort( ( [ , rank1 ], [ , rank2 ] ) => rank2 - rank1 );
return rankedPatterns.map( ( [ pattern ] ) => pattern );
Comment on lines +68 to +75
Copy link
Member Author

Choose a reason for hiding this comment

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

I borrowed this from the searchItems function.

}
8 changes: 8 additions & 0 deletions packages/block-library/src/template-part/editor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@
}
}
}

.block-library-template-part__selection-search {
background: $white;
position: sticky;
top: 0;
padding: $grid-unit-20 0;
z-index: z-index(".block-library-template-part__selection-search");
}