UNPKG

@wordpress/block-library

Version:
342 lines (322 loc) 8.71 kB
/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { memo, useMemo, useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { __, _x } from '@wordpress/i18n'; import { BlockControls, BlockContextProvider, __experimentalUseBlockPreview as useBlockPreview, useBlockProps, useInnerBlocksProps, store as blockEditorStore, } from '@wordpress/block-editor'; import { Spinner, ToolbarGroup } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { list, grid } from '@wordpress/icons'; const TEMPLATE = [ [ 'core/post-title' ], [ 'core/post-date' ], [ 'core/post-excerpt' ], ]; function PostTemplateInnerBlocks( { classList } ) { const innerBlocksProps = useInnerBlocksProps( { className: clsx( 'wp-block-post', classList ) }, { template: TEMPLATE, __unstableDisableLayoutClassNames: true } ); return <li { ...innerBlocksProps } />; } function PostTemplateBlockPreview( { blocks, blockContextId, classList, isHidden, setActiveBlockContextId, } ) { const blockPreviewProps = useBlockPreview( { blocks, props: { className: clsx( 'wp-block-post', classList ), }, } ); const handleOnClick = () => { setActiveBlockContextId( blockContextId ); }; const style = { display: isHidden ? 'none' : undefined, }; return ( <li { ...blockPreviewProps } tabIndex={ 0 } // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role role="button" onClick={ handleOnClick } onKeyPress={ handleOnClick } style={ style } /> ); } const MemoizedPostTemplateBlockPreview = memo( PostTemplateBlockPreview ); export default function PostTemplateEdit( { setAttributes, clientId, context: { query: { perPage, offset = 0, postType, order, orderBy, author, search, exclude, sticky, inherit, taxQuery, parents, pages, format, // We gather extra query args to pass to the REST API call. // This way extenders of Query Loop can add their own query args, // and have accurate previews in the editor. // Noting though that these args should either be supported by the // REST API or be handled by custom REST filters like `rest_{$this->post_type}_query`. ...restQueryArgs } = {}, templateSlug, previewPostType, }, attributes: { layout }, __unstableLayoutClassNames, } ) { const { type: layoutType, columnCount = 3 } = layout || {}; const [ activeBlockContextId, setActiveBlockContextId ] = useState(); const { posts, blocks } = useSelect( ( select ) => { const { getEntityRecords, getTaxonomies } = select( coreStore ); const { getBlocks } = select( blockEditorStore ); const templateCategory = inherit && templateSlug?.startsWith( 'category-' ) && getEntityRecords( 'taxonomy', 'category', { context: 'view', per_page: 1, _fields: [ 'id' ], slug: templateSlug.replace( 'category-', '' ), } ); const templateTag = inherit && templateSlug?.startsWith( 'tag-' ) && getEntityRecords( 'taxonomy', 'post_tag', { context: 'view', per_page: 1, _fields: [ 'id' ], slug: templateSlug.replace( 'tag-', '' ), } ); const query = { offset: offset || 0, order, orderby: orderBy, }; // There is no need to build the taxQuery if we inherit. if ( taxQuery && ! inherit ) { const taxonomies = getTaxonomies( { type: postType, per_page: -1, context: 'view', } ); // We have to build the tax query for the REST API and use as // keys the taxonomies `rest_base` with the `term ids` as values. const builtTaxQuery = Object.entries( taxQuery ).reduce( ( accumulator, [ taxonomySlug, terms ] ) => { const taxonomy = taxonomies?.find( ( { slug } ) => slug === taxonomySlug ); if ( taxonomy?.rest_base ) { accumulator[ taxonomy?.rest_base ] = terms; } return accumulator; }, {} ); if ( !! Object.keys( builtTaxQuery ).length ) { Object.assign( query, builtTaxQuery ); } } if ( perPage ) { query.per_page = perPage; } if ( author ) { query.author = author; } if ( search ) { query.search = search; } if ( exclude?.length ) { query.exclude = exclude; } if ( parents?.length ) { query.parent = parents; } if ( format?.length ) { query.format = format; } /* * Handle cases where sticky is set to `exclude` or `only`. * Which works as a `post__in/post__not_in` query for sticky posts. */ if ( sticky && sticky !== 'ignore' ) { query.sticky = sticky === 'only'; } if ( sticky === 'ignore' ) { // Remove any leftover sticky query parameter. delete query.sticky; query.ignore_sticky = true; } // If `inherit` is truthy, adjust conditionally the query to create a better preview. let currentPostType = postType; if ( inherit ) { // Change the post-type if needed. if ( templateSlug?.startsWith( 'archive-' ) ) { query.postType = templateSlug.replace( 'archive-', '' ); currentPostType = query.postType; } else if ( templateCategory ) { query.categories = templateCategory[ 0 ]?.id; } else if ( templateTag ) { query.tags = templateTag[ 0 ]?.id; } else if ( templateSlug?.startsWith( 'taxonomy-post_format' ) ) { // Get the post format slug from the template slug by removing the prefix. query.format = templateSlug.replace( 'taxonomy-post_format-post-format-', '' ); } } // When we preview Query Loop blocks we should prefer the current // block's postType, which is passed through block context. const usedPostType = previewPostType || currentPostType; return { posts: getEntityRecords( 'postType', usedPostType, { ...query, ...restQueryArgs, } ), blocks: getBlocks( clientId ), }; }, [ perPage, offset, order, orderBy, clientId, author, search, postType, exclude, sticky, inherit, templateSlug, taxQuery, parents, format, restQueryArgs, previewPostType, ] ); const blockContexts = useMemo( () => posts?.map( ( post ) => ( { postType: post.type, postId: post.id, classList: post.class_list ?? '', } ) ), [ posts ] ); const blockProps = useBlockProps( { className: clsx( __unstableLayoutClassNames, { [ `columns-${ columnCount }` ]: layoutType === 'grid' && columnCount, // Ensure column count is flagged via classname for backwards compatibility. } ), } ); if ( ! posts ) { return ( <p { ...blockProps }> <Spinner /> </p> ); } if ( ! posts.length ) { return <p { ...blockProps }> { __( 'No results found.' ) }</p>; } const setDisplayLayout = ( newDisplayLayout ) => setAttributes( { layout: { ...layout, ...newDisplayLayout }, } ); const displayLayoutControls = [ { icon: list, title: _x( 'List view', 'Post template block display setting' ), onClick: () => setDisplayLayout( { type: 'default' } ), isActive: layoutType === 'default' || layoutType === 'constrained', }, { icon: grid, title: _x( 'Grid view', 'Post template block display setting' ), onClick: () => setDisplayLayout( { type: 'grid', columnCount, } ), isActive: layoutType === 'grid', }, ]; // To avoid flicker when switching active block contexts, a preview is rendered // for each block context, but the preview for the active block context is hidden. // This ensures that when it is displayed again, the cached rendering of the // block preview is used, instead of having to re-render the preview from scratch. return ( <> <BlockControls> <ToolbarGroup controls={ displayLayoutControls } /> </BlockControls> <ul { ...blockProps }> { blockContexts && blockContexts.map( ( blockContext ) => ( <BlockContextProvider key={ blockContext.postId } value={ blockContext } > { blockContext.postId === ( activeBlockContextId || blockContexts[ 0 ]?.postId ) ? ( <PostTemplateInnerBlocks classList={ blockContext.classList } /> ) : null } <MemoizedPostTemplateBlockPreview blocks={ blocks } blockContextId={ blockContext.postId } classList={ blockContext.classList } setActiveBlockContextId={ setActiveBlockContextId } isHidden={ blockContext.postId === ( activeBlockContextId || blockContexts[ 0 ]?.postId ) } /> </BlockContextProvider> ) ) } </ul> </> ); }