@wordpress/block-library
Version:
Block library for the WordPress editor.
342 lines (322 loc) • 8.71 kB
JavaScript
/**
* 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>
</>
);
}