diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index a1811e85dba6a..ca9b3898d1d37 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -21,6 +21,7 @@ import InserterSearchForm from './search-form'; import InserterPreviewPanel from './preview-panel'; import InserterBlockList from './block-list'; import BlockPatterns from './block-patterns'; +import TemplateParts from './template-parts'; const stopKeyPropagation = ( event ) => event.stopPropagation(); @@ -41,6 +42,7 @@ function InserterMenu( { getBlockIndex, getBlockSelectionEnd, getBlockOrder, + hasTemplateParts, } = useSelect( ( select ) => { const { @@ -57,6 +59,8 @@ function InserterMenu( { } } return { + hasTemplateParts: getSettings() + .__experimentalEnableFullSiteEditing, hasPatterns: !! getSettings().__experimentalBlockPatterns ?.length, destinationRootClientId: destRootClientId, @@ -172,6 +176,36 @@ function InserterMenu( { ); + const templatePartsTab = ( +
+ +
+ ); + + const tabsToUse = [ + { + name: 'blocks', + /* translators: Blocks tab title in the block inserter. */ + title: __( 'Blocks' ), + }, + ]; + if ( showPatterns ) { + tabsToUse.push( { + name: 'patterns', + /* translators: Patterns tab title in the block inserter. */ + title: __( 'Patterns' ), + } ); + } + if ( hasTemplateParts ) { + tabsToUse.push( { + name: 'template parts', + /* translators: Template Parts tab title in the block inserter. */ + title: __( 'Template Parts' ), + } ); + } // Disable reason (no-autofocus): The inserter menu is a modal display, not one which // is always visible, and one which already incurs this behavior of autoFocus via // Popover's focusOnMount. @@ -186,31 +220,22 @@ function InserterMenu( { >
- { showPatterns && ( + { tabsToUse.length > 1 && ( { ( tab ) => { if ( tab.name === 'blocks' ) { return blocksTab; + } else if ( tab.name === 'template parts' ) { + return templatePartsTab; } return patternsTab; } } ) } - { ! showPatterns && blocksTab } + { tabsToUse.length === 1 && blocksTab }
{ showInserterHelpPanel && hoveredItem && (
diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index f7557d631f2bd..4a4b786544ded 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -247,7 +247,8 @@ $block-inserter-tabs-height: 44px; flex-shrink: 0; } -.block-editor-inserter__patterns-item { +.block-editor-inserter__patterns-item, +.block-editor-inserter__template-part-item { border-radius: $radius-block-ui; cursor: pointer; margin-top: $grid-unit-20; @@ -271,7 +272,8 @@ $block-inserter-tabs-height: 44px; } } -.block-editor-inserter__patterns-item-title { +.block-editor-inserter__patterns-item-title, +.block-editor-inserter__template-part-item-title { padding: $grid-unit-05; font-size: 12px; text-align: center; diff --git a/packages/block-editor/src/components/inserter/template-parts.js b/packages/block-editor/src/components/inserter/template-parts.js new file mode 100644 index 0000000000000..d3ffbfdc24b85 --- /dev/null +++ b/packages/block-editor/src/components/inserter/template-parts.js @@ -0,0 +1,197 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { parse, createBlock } from '@wordpress/blocks'; +import { useMemo, useCallback } from '@wordpress/element'; +import { ENTER, SPACE } from '@wordpress/keycodes'; +import { __, sprintf } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import BlockPreview from '../block-preview'; +import InserterPanel from './panel'; +import useAsyncList from './use-async-list'; + +/** + * External dependencies + */ +import { groupBy } from 'lodash'; + +function TemplatePartPlaceholder() { + return ( +
+ ); +} + +function TemplatePartItem( { templatePart, onInsert } ) { + const { id, slug, theme } = templatePart; + // The 'raw' property is not defined for a brief period in the save cycle. + // The fallback prevents an error in the parse function while saving. + const content = templatePart.content.raw || ''; + const blocks = useMemo( () => parse( content ), [ content ] ); + const { createSuccessNotice } = useDispatch( 'core/notices' ); + + const onClick = useCallback( () => { + const templatePartBlock = createBlock( 'core/template-part', { + postId: id, + slug, + theme, + } ); + onInsert( templatePartBlock ); + createSuccessNotice( + sprintf( + /* translators: %s: template part title. */ + __( 'Template Part "%s" inserted.' ), + slug + ), + { + type: 'snackbar', + } + ); + }, [ id, slug, theme ] ); + + return ( +
{ + if ( ENTER === event.keyCode || SPACE === event.keyCode ) { + onClick(); + } + } } + tabIndex={ 0 } + aria-label={ templatePart.slug } + > + +
+ { templatePart.slug } +
+
+ ); +} + +function TemplatePartsByTheme( { templateParts, onInsert } ) { + const templatePartsByTheme = useMemo( () => { + return Object.values( groupBy( templateParts, 'meta.theme' ) ); + }, [ templateParts ] ); + const currentShownTPs = useAsyncList( templateParts ); + + return ( + <> + { templatePartsByTheme.length && + templatePartsByTheme.map( ( templatePartList ) => ( + + { templatePartList.map( ( templatePart ) => { + return currentShownTPs.includes( templatePart ) ? ( + + ) : ( + + ); + } ) } + + ) ) } + + ); +} + +function TemplatePartSearchResults( { templateParts, onInsert, filterValue } ) { + const filteredTPs = useMemo( () => { + // Filter based on value. + const lowerFilterValue = filterValue.toLowerCase(); + const searchResults = templateParts.filter( + ( { slug, meta: { theme } } ) => + slug.toLowerCase().includes( lowerFilterValue ) || + theme.toLowerCase().includes( lowerFilterValue ) + ); + // Order based on value location. + searchResults.sort( ( a, b ) => { + // First prioritize index found in slug. + const indexInSlugA = a.slug + .toLowerCase() + .indexOf( lowerFilterValue ); + const indexInSlugB = b.slug + .toLowerCase() + .indexOf( lowerFilterValue ); + if ( indexInSlugA !== -1 && indexInSlugB !== -1 ) { + return indexInSlugA - indexInSlugB; + } else if ( indexInSlugA !== -1 ) { + return -1; + } else if ( indexInSlugB !== -1 ) { + return 1; + } + // Second prioritize index found in theme. + return ( + a.meta.theme.toLowerCase().indexOf( lowerFilterValue ) - + b.meta.theme.toLowerCase().indexOf( lowerFilterValue ) + ); + } ); + return searchResults; + }, [ filterValue, templateParts ] ); + + const currentShownTPs = useAsyncList( filteredTPs ); + + return ( + <> + { filteredTPs.map( ( templatePart ) => ( + + { currentShownTPs.includes( templatePart ) ? ( + + ) : ( + + ) } + + ) ) } + + ); +} + +export default function TemplateParts( { onInsert, filterValue } ) { + const templateParts = useSelect( ( select ) => { + return select( 'core' ).getEntityRecords( + 'postType', + 'wp_template_part', + { + status: [ 'publish', 'auto-draft' ], + } + ); + }, [] ); + + if ( ! templateParts || ! templateParts.length ) { + return null; + } + + if ( filterValue ) { + return ( + + ); + } + + return ( + + ); +}