-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Add Term Template block #70747
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
base: add/terms-query-block-container
Are you sure you want to change the base?
Add Term Template block #70747
Changes from 15 commits
603f0c1
597850d
605b487
657a734
120e94b
4e2febb
79a4884
58b51ae
8f8bebf
dbe203b
7c89c9a
eca49cc
f857987
b8f0d7a
1615969
c055e78
bdf31b2
ef863fd
0f82d3b
7d9b497
4335916
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
{ | ||
"$schema": "https://schemas.wp.org/trunk/block.json", | ||
"apiVersion": 3, | ||
"__experimental": true, | ||
"name": "core/term-template", | ||
"title": "Term Template", | ||
"category": "theme", | ||
"ancestor": [ "core/terms-query" ], | ||
"description": "Contains the block elements used to render a taxonomy term, like the name, description, and more.", | ||
"textdomain": "default", | ||
"usesContext": [ "termQueryId", "termQuery", "templateSlug", "termName" ], | ||
"supports": { | ||
"reusable": false, | ||
"html": false, | ||
"align": [ "wide", "full" ], | ||
"layout": true, | ||
"color": { | ||
"gradients": true, | ||
"link": true, | ||
"__experimentalDefaultControls": { | ||
"background": true, | ||
"text": true | ||
} | ||
}, | ||
"typography": { | ||
"fontSize": true, | ||
"lineHeight": true, | ||
"__experimentalFontFamily": true, | ||
"__experimentalFontWeight": true, | ||
"__experimentalFontStyle": true, | ||
"__experimentalTextTransform": true, | ||
"__experimentalTextDecoration": true, | ||
"__experimentalLetterSpacing": true, | ||
"__experimentalDefaultControls": { | ||
"fontSize": true | ||
} | ||
}, | ||
"spacing": { | ||
"margin": true, | ||
"padding": true, | ||
"blockGap": { | ||
"__experimentalDefault": "1.25em" | ||
}, | ||
"__experimentalDefaultControls": { | ||
"blockGap": true, | ||
"padding": false, | ||
"margin": false | ||
} | ||
}, | ||
"interactivity": { | ||
"clientNavigation": true | ||
}, | ||
"__experimentalBorder": { | ||
"radius": true, | ||
"color": true, | ||
"width": true, | ||
"style": true | ||
} | ||
}, | ||
"allowedBlocks": [ "core/paragraph" ], | ||
"style": "wp-block-term-template" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import clsx from 'clsx'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import { memo, useMemo, useState } from '@wordpress/element'; | ||
import { useSelect } from '@wordpress/data'; | ||
import { __ } 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 { useEntityRecords } from '@wordpress/core-data'; | ||
|
||
const TEMPLATE = [ [ 'core/paragraph' ] ]; | ||
|
||
function TermTemplateInnerBlocks( { classList, term } ) { | ||
const innerBlocksProps = useInnerBlocksProps( | ||
{ className: clsx( 'wp-block-term', classList ) }, | ||
{ template: TEMPLATE, __unstableDisableLayoutClassNames: true } | ||
); | ||
return <li { ...innerBlocksProps }>{ term?.name }</li>; | ||
} | ||
|
||
function TermTemplateBlockPreview( { | ||
blocks, | ||
blockContextId, | ||
classList, | ||
isHidden, | ||
setActiveBlockContextId, | ||
termName, | ||
} ) { | ||
const blockPreviewProps = useBlockPreview( { | ||
blocks, | ||
props: { | ||
className: clsx( 'wp-block-term', 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 } | ||
> | ||
{ termName } | ||
</li> | ||
); | ||
} | ||
|
||
const MemoizedTermTemplateBlockPreview = memo( TermTemplateBlockPreview ); | ||
|
||
/** | ||
* Builds a hierarchical tree structure from flat terms array. | ||
* | ||
* @param {Array} terms Array of term objects. | ||
* @return {Array} Tree structure with parent/child relationships. | ||
*/ | ||
function buildTermsTree( terms ) { | ||
const termsById = {}; | ||
const rootTerms = []; | ||
|
||
terms.forEach( ( term ) => { | ||
termsById[ term.id ] = { | ||
term, | ||
children: [], | ||
}; | ||
} ); | ||
|
||
terms.forEach( ( term ) => { | ||
if ( term.parent && termsById[ term.parent ] ) { | ||
termsById[ term.parent ].children.push( termsById[ term.id ] ); | ||
} else { | ||
rootTerms.push( termsById[ term.id ] ); | ||
} | ||
} ); | ||
|
||
return rootTerms; | ||
} | ||
|
||
/** | ||
* Renders a single term node and its children recursively. | ||
* | ||
* @param {Object} termNode Term node with term object and children. | ||
* @param {Function} renderTerm Function to render a single term. | ||
* @return {JSX.Element} Rendered term node. | ||
*/ | ||
function renderTermNode( termNode, renderTerm ) { | ||
const children = | ||
termNode.children.length > 0 ? ( | ||
<ul> | ||
{ termNode.children.map( ( childNode ) => | ||
renderTermNode( childNode, renderTerm ) | ||
) } | ||
</ul> | ||
) : null; | ||
|
||
return ( | ||
<> | ||
{ renderTerm( termNode.term ) } | ||
{ children } | ||
</> | ||
); | ||
} | ||
|
||
export default function TermTemplateEdit( { | ||
clientId, | ||
context: { | ||
termQuery: { | ||
taxonomy, | ||
order, | ||
orderBy, | ||
hideEmpty, | ||
hierarchical, | ||
parent, | ||
} = {}, | ||
}, | ||
} ) { | ||
const [ activeBlockContextId, setActiveBlockContextId ] = useState(); | ||
|
||
const queryArgs = { | ||
order, | ||
orderby: orderBy, | ||
hide_empty: hideEmpty, | ||
}; | ||
|
||
const { records: terms, isResolving } = useEntityRecords( | ||
'taxonomy', | ||
taxonomy, | ||
queryArgs | ||
); | ||
|
||
// Filter to show only top-level terms if "Show only top-level terms" is enabled. | ||
const filteredTerms = useMemo( () => { | ||
if ( ! terms || parent !== 0 ) { | ||
return terms; | ||
} | ||
return terms.filter( ( term ) => ! term.parent ); | ||
}, [ terms, parent ] ); | ||
|
||
const { blocks } = useSelect( | ||
( select ) => ( { | ||
blocks: select( blockEditorStore ).getBlocks( clientId ), | ||
} ), | ||
[ clientId ] | ||
); | ||
|
||
const blockContexts = useMemo( | ||
() => | ||
filteredTerms?.map( ( term ) => ( { | ||
taxonomy, | ||
termId: term.id, | ||
classList: `term-${ term.id }`, | ||
} ) ), | ||
[ filteredTerms, taxonomy ] | ||
); | ||
|
||
const blockProps = useBlockProps( { | ||
className: clsx( 'wp-block-term-template' ), | ||
mikachan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} ); | ||
|
||
if ( isResolving ) { | ||
return ( | ||
<p { ...blockProps }> | ||
<Spinner /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice idea, thanks! I've added some grey placeholders in list items in 0f82d3b: Screen.Recording.2025-08-01.at.12.40.53.movI've used a similar animation to what's currently used in the Navigation and LinkControl blocks. It might need some tweaking but I prefer this to the spinner. |
||
</p> | ||
); | ||
} | ||
|
||
if ( ! filteredTerms?.length ) { | ||
return <p { ...blockProps }> { __( 'No terms found.' ) }</p>; | ||
} | ||
|
||
const renderTerm = ( term ) => { | ||
const blockContext = { | ||
taxonomy, | ||
termId: term.id, | ||
classList: `term-${ term.id }`, | ||
}; | ||
|
||
return ( | ||
<BlockContextProvider key={ term.id } value={ blockContext }> | ||
{ term.id === | ||
mikachan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
( activeBlockContextId || blockContexts[ 0 ]?.termId ) ? ( | ||
<TermTemplateInnerBlocks | ||
classList={ blockContext.classList } | ||
term={ term } | ||
/> | ||
) : null } | ||
<MemoizedTermTemplateBlockPreview | ||
blocks={ blocks } | ||
blockContextId={ term.id } | ||
classList={ blockContext.classList } | ||
setActiveBlockContextId={ setActiveBlockContextId } | ||
isHidden={ | ||
term.id === | ||
( activeBlockContextId || blockContexts[ 0 ]?.termId ) | ||
} | ||
termName={ term.name } | ||
/> | ||
</BlockContextProvider> | ||
); | ||
}; | ||
|
||
const renderTerms = () => { | ||
if ( hierarchical ) { | ||
const termsTree = buildTermsTree( filteredTerms ); | ||
return termsTree.map( ( termNode ) => | ||
renderTermNode( termNode, renderTerm ) | ||
); | ||
} | ||
|
||
return blockContexts.map( ( blockContext ) => ( | ||
<BlockContextProvider | ||
key={ blockContext.termId } | ||
value={ blockContext } | ||
> | ||
{ blockContext.termId === | ||
( activeBlockContextId || blockContexts[ 0 ]?.termId ) ? ( | ||
<TermTemplateInnerBlocks | ||
classList={ blockContext.classList } | ||
term={ filteredTerms.find( | ||
( t ) => t.id === blockContext.termId | ||
) } | ||
/> | ||
) : null } | ||
<MemoizedTermTemplateBlockPreview | ||
blocks={ blocks } | ||
blockContextId={ blockContext.termId } | ||
classList={ blockContext.classList } | ||
setActiveBlockContextId={ setActiveBlockContextId } | ||
isHidden={ | ||
blockContext.termId === | ||
( activeBlockContextId || blockContexts[ 0 ]?.termId ) | ||
} | ||
termName={ | ||
filteredTerms.find( | ||
( t ) => t.id === blockContext.termId | ||
)?.name | ||
} | ||
/> | ||
</BlockContextProvider> | ||
) ); | ||
}; | ||
|
||
return ( | ||
<> | ||
<BlockControls> | ||
<ToolbarGroup /> | ||
</BlockControls> | ||
|
||
<div { ...blockProps }> | ||
<ul>{ renderTerms() }</ul> | ||
</div> | ||
</> | ||
); | ||
} |
Uh oh!
There was an error while loading. Please reload this page.