diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index fc81520a2b7238..a23ed2f9934e27 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -991,6 +991,15 @@ Display the description of categories, tags and custom taxonomies when viewing a - **Supports:** align (full, wide), color (background, link, text), interactivity (clientNavigation), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** textAlign +## Term Name + +Displays the name of a taxonomy term. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/term-name)) + +- **Name:** core/term-name +- **Category:** theme +- **Supports:** align (full, wide), color (background, gradients, link, text), interactivity (clientNavigation), spacing (padding), typography (fontSize, lineHeight), ~~html~~ +- **Attributes:** isLink, level, textAlign + ## Term Template Contains the block elements used to render a taxonomy term, like the name, description, and more. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/term-template)) diff --git a/lib/blocks.php b/lib/blocks.php index 3a980b22fd5442..236011b03abd0c 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -125,6 +125,7 @@ function gutenberg_reregister_core_block_types() { 'tag-cloud.php' => 'core/tag-cloud', 'template-part.php' => 'core/template-part', 'term-description.php' => 'core/term-description', + 'term-name.php' => 'core/term-name', 'terms-query.php' => 'core/terms-query', 'term-template.php' => 'core/term-template', 'video.php' => 'core/video', diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index 590e3da0330882..e56c6c6b00d32c 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -129,6 +129,7 @@ import * as tableOfContents from './table-of-contents'; import * as tagCloud from './tag-cloud'; import * as templatePart from './template-part'; import * as termDescription from './term-description'; +import * as termName from './term-name'; import * as termsQuery from './terms-query'; import * as termTemplate from './term-template'; import * as textColumns from './text-columns'; @@ -250,6 +251,7 @@ const getAllBlocks = () => { homeLink, logInOut, termDescription, + termName, queryTitle, postAuthorBiography, ]; diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss index e322f2f027862f..feb673ae47a477 100644 --- a/packages/block-library/src/style.scss +++ b/packages/block-library/src/style.scss @@ -71,6 +71,7 @@ @import "./table/style.scss"; @import "./table-of-contents/style.scss"; @import "./term-description/style.scss"; +@import "./term-name/style.scss"; @import "./term-template/style.scss"; @import "./text-columns/style.scss"; @import "./verse/style.scss"; diff --git a/packages/block-library/src/term-name/block.json b/packages/block-library/src/term-name/block.json new file mode 100644 index 00000000000000..29b0cbe22b77e5 --- /dev/null +++ b/packages/block-library/src/term-name/block.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "core/term-name", + "title": "Term Name", + "category": "theme", + "description": "Displays the name of a taxonomy term.", + "keywords": [ "term title" ], + "textdomain": "default", + "usesContext": [ "termId", "taxonomy" ], + "attributes": { + "textAlign": { + "type": "string" + }, + "level": { + "type": "number", + "default": 0 + }, + "isLink": { + "type": "boolean", + "default": false + } + }, + "supports": { + "align": [ "wide", "full" ], + "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, + "spacing": { + "padding": true + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalFontWeight": true, + "__experimentalFontStyle": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalLetterSpacing": true, + "__experimentalDefaultControls": { + "fontSize": true + } + }, + "interactivity": { + "clientNavigation": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "color": true, + "width": true, + "style": true + } + } + }, + "style": "wp-block-term-name" +} diff --git a/packages/block-library/src/term-name/edit.js b/packages/block-library/src/term-name/edit.js new file mode 100644 index 00000000000000..dd3d7c9726edb0 --- /dev/null +++ b/packages/block-library/src/term-name/edit.js @@ -0,0 +1,108 @@ +/** + * External dependencies + */ +import clsx from 'clsx'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + BlockControls, + AlignmentControl, + InspectorControls, + HeadingLevelDropdown, +} from '@wordpress/block-editor'; +import { + ToggleControl, + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { useToolsPanelDropdownMenuProps } from '../utils/hooks'; +import { useTermName } from './use-term-name'; + +export default function TermNameEdit( { + attributes, + setAttributes, + context: { termId, taxonomy }, +} ) { + const { textAlign, level = 0, isLink } = attributes; + const { term } = useTermName( termId, taxonomy ); + + const termName = term?.name || __( 'Term Name' ); + + const blockProps = useBlockProps( { + className: clsx( { + [ `has-text-align-${ textAlign }` ]: textAlign, + } ), + } ); + + const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + + const TagName = level === 0 ? 'p' : `h${ level }`; + + let termNameDisplay = termName; + if ( isLink ) { + termNameDisplay = ( + e.preventDefault() } + > + { termName } + + ); + } + + return ( + <> + + { + setAttributes( { level: newLevel } ); + } } + /> + { + setAttributes( { textAlign: nextAlign } ); + } } + /> + + + { + setAttributes( { + isLink: false, + } ); + } } + dropdownMenuProps={ dropdownMenuProps } + > + !! isLink } + label={ __( 'Make term name a link' ) } + onDeselect={ () => setAttributes( { isLink: false } ) } + isShownByDefault + > + + setAttributes( { isLink: ! isLink } ) + } + checked={ isLink } + /> + + + + { termNameDisplay } + + ); +} diff --git a/packages/block-library/src/term-name/index.js b/packages/block-library/src/term-name/index.js new file mode 100644 index 00000000000000..f74cd380dbd3d9 --- /dev/null +++ b/packages/block-library/src/term-name/index.js @@ -0,0 +1,21 @@ +/** + * WordPress dependencies + */ +import { termName as icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import initBlock from '../utils/init-block'; +import metadata from './block.json'; +import edit from './edit'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + icon, + edit, +}; + +export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/term-name/index.php b/packages/block-library/src/term-name/index.php new file mode 100644 index 00000000000000..3a2bf18bb5f210 --- /dev/null +++ b/packages/block-library/src/term-name/index.php @@ -0,0 +1,81 @@ +context['termId'] ) && isset( $block->context['taxonomy'] ) ) { + $term = get_term( $block->context['termId'], $block->context['taxonomy'] ); + } else { + $term = get_queried_object(); + if ( ! $term instanceof WP_Term ) { + $term = null; + } + } + + if ( ! $term || is_wp_error( $term ) ) { + return ''; + } + + $term_name = $term->name; + $level = isset( $attributes['level'] ) ? $attributes['level'] : 0; + $tag_name = 0 === $level ? 'p' : 'h' . (int) $level; + + if ( isset( $attributes['isLink'] ) && $attributes['isLink'] ) { + $term_link = get_term_link( $term ); + if ( ! is_wp_error( $term_link ) ) { + $term_name = sprintf( + '%2$s', + esc_url( $term_link ), + $term_name + ); + } + } + + $classes = array(); + if ( isset( $attributes['textAlign'] ) ) { + $classes[] = 'has-text-align-' . $attributes['textAlign']; + } + if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) { + $classes[] = 'has-link-color'; + } + $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => implode( ' ', $classes ) ) ); + + return sprintf( + '<%1$s %2$s>%3$s', + $tag_name, + $wrapper_attributes, + $term_name + ); +} + +/** + * Registers the `core/term-name` block on the server. + * + * @since 6.9.0 + */ +function register_block_core_term_name() { + register_block_type_from_metadata( + __DIR__ . '/term-name', + array( + 'render_callback' => 'render_block_core_term_name', + ) + ); +} +add_action( 'init', 'register_block_core_term_name' ); diff --git a/packages/block-library/src/term-name/style.scss b/packages/block-library/src/term-name/style.scss new file mode 100644 index 00000000000000..b3e068055cd326 --- /dev/null +++ b/packages/block-library/src/term-name/style.scss @@ -0,0 +1,4 @@ +.wp-block-term-name { + // This block has customizable padding, border-box makes that more predictable. + box-sizing: border-box; +} diff --git a/packages/block-library/src/term-name/use-term-name.js b/packages/block-library/src/term-name/use-term-name.js new file mode 100644 index 00000000000000..42693b02a218b4 --- /dev/null +++ b/packages/block-library/src/term-name/use-term-name.js @@ -0,0 +1,106 @@ +/** + * WordPress dependencies + */ +import { store as coreStore } from '@wordpress/core-data'; +import { useSelect } from '@wordpress/data'; + +/** + * Hook to fetch term name based on context or fallback to template parsing. + * + * This hook prioritizes context-provided termId and taxonomy, but falls back to + * template-based detection when no context is available. + * + * @param {string|number} termId The term ID from context + * @param {string} taxonomy The taxonomy name from context + */ +export function useTermName( termId, taxonomy ) { + // Get term from context if available. + const contextBasedTerm = useSelect( + ( select ) => { + if ( ! termId || ! taxonomy ) { + return null; + } + return select( coreStore ).getEntityRecord( + 'taxonomy', + taxonomy, + termId + ); + }, + [ termId, taxonomy ] + ); + + // Fallback approach: Parse template slug when no context is available. + const templateBasedTerm = useTemplateBasedTermData(); + const hasContext = Boolean( termId && taxonomy ); + + return { + hasContext, + term: hasContext ? contextBasedTerm : templateBasedTerm, + }; +} + +/** + * Fallback hook to fetch term data from template context. + * Parses the template slug to determine if we're on a specific term archive. + */ +function useTemplateBasedTermData() { + const templateSlug = useSelect( ( select ) => { + // Access core/editor by string to avoid @wordpress/editor dependency. + // eslint-disable-next-line @wordpress/data-no-store-string-literals + const { getCurrentPostId, getCurrentPostType, getCurrentTemplateId } = + select( 'core/editor' ); + const currentPostType = getCurrentPostType(); + const templateId = + getCurrentTemplateId() || + ( currentPostType === 'wp_template' ? getCurrentPostId() : null ); + + return templateId + ? select( coreStore ).getEditedEntityRecord( + 'postType', + 'wp_template', + templateId + )?.slug + : null; + }, [] ); + + const taxonomyMatches = templateSlug?.match( + /^(category|tag|taxonomy-([^-]+))$|^(((category|tag)|taxonomy-([^-]+))-(.+))$/ + ); + + let taxonomy; + let termSlug; + + if ( taxonomyMatches ) { + // If it's for a specific term (e.g., category-news, tag-featured). + if ( taxonomyMatches[ 3 ] ) { + taxonomy = taxonomyMatches[ 6 ] + ? taxonomyMatches[ 6 ] + : taxonomyMatches[ 4 ]; + termSlug = taxonomyMatches[ 7 ]; + } + + taxonomy = taxonomy === 'tag' ? 'post_tag' : taxonomy; + } + + return useSelect( + ( select ) => { + if ( ! taxonomy || ! termSlug ) { + return null; + } + + const { getEntityRecords } = select( coreStore ); + + const termRecords = getEntityRecords( 'taxonomy', taxonomy, { + slug: termSlug, + per_page: 1, + } ); + + if ( termRecords && termRecords[ 0 ] ) { + return termRecords[ 0 ]; + } + + return null; + }, + [ taxonomy, termSlug ] + ); +} diff --git a/packages/icons/src/library/term-name.svg b/packages/icons/src/library/term-name.svg new file mode 100644 index 00000000000000..c4135c2511de24 --- /dev/null +++ b/packages/icons/src/library/term-name.svg @@ -0,0 +1,4 @@ + + + + diff --git a/test/integration/fixtures/blocks/core__term-name.html b/test/integration/fixtures/blocks/core__term-name.html new file mode 100644 index 00000000000000..062708273d3db5 --- /dev/null +++ b/test/integration/fixtures/blocks/core__term-name.html @@ -0,0 +1 @@ + diff --git a/test/integration/fixtures/blocks/core__term-name.json b/test/integration/fixtures/blocks/core__term-name.json new file mode 100644 index 00000000000000..8b2282f90959eb --- /dev/null +++ b/test/integration/fixtures/blocks/core__term-name.json @@ -0,0 +1,11 @@ +[ + { + "name": "core/term-name", + "isValid": true, + "attributes": { + "level": 0, + "isLink": false + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__term-name.parsed.json b/test/integration/fixtures/blocks/core__term-name.parsed.json new file mode 100644 index 00000000000000..d13cb26fcb4eb0 --- /dev/null +++ b/test/integration/fixtures/blocks/core__term-name.parsed.json @@ -0,0 +1,9 @@ +[ + { + "blockName": "core/term-name", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + } +] diff --git a/test/integration/fixtures/blocks/core__term-name.serialized.html b/test/integration/fixtures/blocks/core__term-name.serialized.html new file mode 100644 index 00000000000000..062708273d3db5 --- /dev/null +++ b/test/integration/fixtures/blocks/core__term-name.serialized.html @@ -0,0 +1 @@ +