@wordpress/block-library
Version:
Block library for the WordPress editor.
280 lines (259 loc) • 7.52 kB
JavaScript
/**
* WordPress dependencies
*/
import {
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem,
CheckboxControl,
TextControl,
TextareaControl,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor';
import { useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { useToolsPanelDropdownMenuProps } from '../../utils/hooks';
import { useHandleLinkChange } from './use-handle-link-change';
import { useEntityBinding } from './use-entity-binding';
import { getSuggestionsQuery } from '../link-ui';
import { useLinkPreview } from './use-link-preview';
import { unlock } from '../../lock-unlock';
const { LinkPicker } = unlock( blockEditorPrivateApis );
/**
* Get a human-readable entity type name.
*
* @param {string} type - The entity type
* @param {string} kind - The entity kind
* @return {string} Human-readable entity type name
*/
function getEntityTypeName( type, kind ) {
if ( kind === 'post-type' ) {
switch ( type ) {
case 'post':
return __( 'post' );
case 'page':
return __( 'page' );
default:
return type || __( 'post' );
}
}
if ( kind === 'taxonomy' ) {
switch ( type ) {
case 'category':
return __( 'category' );
case 'tag':
return __( 'tag' );
default:
return type || __( 'term' );
}
}
return type || __( 'item' );
}
/**
* Shared Controls component for Navigation Link and Navigation Submenu blocks.
*
* This component provides the inspector controls (ToolsPanel) that are identical
* between both navigation blocks.
*
* @param {Object} props - Component props
* @param {Object} props.attributes - Block attributes
* @param {Function} props.setAttributes - Function to update block attributes
* @param {string} props.clientId - Block client ID
*/
export function Controls( { attributes, setAttributes, clientId } ) {
const { label, url, description, rel, opensInNewTab } = attributes;
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
// Use the entity binding hook for UI state (help text, link preview, etc.)
const { hasUrlBinding, isBoundEntityAvailable, entityRecord } =
useEntityBinding( {
clientId,
attributes,
} );
const needsHelpText = hasUrlBinding;
const helpText = isBoundEntityAvailable
? BindingHelpText( {
type: attributes.type,
kind: attributes.kind,
} )
: MissingEntityHelpText( {
type: attributes.type,
kind: attributes.kind,
} );
// Get the link change handler with built-in binding management
const handleLinkChange = useHandleLinkChange( {
clientId,
attributes,
setAttributes,
} );
const linkTitle =
entityRecord?.title?.rendered ||
entityRecord?.title ||
entityRecord?.name;
const linkImage = useSelect(
( select ) => {
// Only fetch for post-type entities with featured media
if ( ! entityRecord?.featured_media ) {
return null;
}
const { getEntityRecord } = select( coreStore );
// Get the media entity to fetch the image URL
const media = getEntityRecord(
'postType',
'attachment',
entityRecord.featured_media
);
// Return the thumbnail or medium size URL, fallback to source_url
return (
media?.media_details?.sizes?.thumbnail?.source_url ||
media?.media_details?.sizes?.medium?.source_url ||
media?.source_url ||
null
);
},
[ entityRecord?.featured_media ]
);
const preview = useLinkPreview( {
url,
title: linkTitle,
image: linkImage,
type: attributes.type,
entityStatus: entityRecord?.status,
hasBinding: hasUrlBinding,
isEntityAvailable: isBoundEntityAvailable,
} );
return (
<ToolsPanel
label={ __( 'Settings' ) }
resetAll={ () => {
setAttributes( {
label: '',
url: '',
description: '',
rel: '',
opensInNewTab: false,
} );
} }
dropdownMenuProps={ dropdownMenuProps }
>
<ToolsPanelItem
hasValue={ () => !! label }
label={ __( 'Text' ) }
onDeselect={ () => setAttributes( { label: '' } ) }
isShownByDefault
>
<TextControl
__next40pxDefaultSize
label={ __( 'Text' ) }
value={ label ? stripHTML( label ) : '' }
onChange={ ( labelValue ) => {
setAttributes( { label: labelValue } );
} }
autoComplete="off"
/>
</ToolsPanelItem>
<ToolsPanelItem
hasValue={ () => !! url }
label={ __( 'Link to' ) }
onDeselect={ () => setAttributes( { url: '' } ) }
isShownByDefault
>
<LinkPicker
preview={ preview }
onSelect={ handleLinkChange }
suggestionsQuery={ getSuggestionsQuery(
attributes.type,
attributes.kind
) }
label={ __( 'Link to' ) }
help={ needsHelpText ? helpText : undefined }
/>
</ToolsPanelItem>
<ToolsPanelItem
hasValue={ () => !! opensInNewTab }
label={ __( 'Open in new tab' ) }
onDeselect={ () => setAttributes( { opensInNewTab: false } ) }
isShownByDefault
>
<CheckboxControl
label={ __( 'Open in new tab' ) }
checked={ opensInNewTab }
onChange={ ( value ) =>
setAttributes( { opensInNewTab: value } )
}
/>
</ToolsPanelItem>
<ToolsPanelItem
hasValue={ () => !! description }
label={ __( 'Description' ) }
onDeselect={ () => setAttributes( { description: '' } ) }
isShownByDefault
>
<TextareaControl
label={ __( 'Description' ) }
value={ description || '' }
onChange={ ( descriptionValue ) => {
setAttributes( { description: descriptionValue } );
} }
help={ __(
'The description will be displayed in the menu if the current theme supports it.'
) }
/>
</ToolsPanelItem>
<ToolsPanelItem
hasValue={ () => !! rel }
label={ __( 'Rel attribute' ) }
onDeselect={ () => setAttributes( { rel: '' } ) }
isShownByDefault
>
<TextControl
__next40pxDefaultSize
label={ __( 'Rel attribute' ) }
value={ rel || '' }
onChange={ ( relValue ) => {
setAttributes( { rel: relValue } );
} }
autoComplete="off"
help={ __(
'The relationship of the linked URL as space-separated link types.'
) }
/>
</ToolsPanelItem>
</ToolsPanel>
);
}
/**
* Component to display help text for bound URL attributes.
*
* @param {Object} props - Component props
* @param {string} props.type - The entity type
* @param {string} props.kind - The entity kind
* @return {string} Help text for the bound URL
*/
export function BindingHelpText( { type, kind } ) {
const entityType = getEntityTypeName( type, kind );
return sprintf(
/* translators: %s is the entity type (e.g., "page", "post", "category") */
__( 'Synced with the selected %s.' ),
entityType
);
}
/**
* Component to display error help text for missing entity bindings.
*
* @param {Object} props - Component props
* @param {string} props.type - The entity type
* @param {string} props.kind - The entity kind
* @return {JSX.Element} Error help text component
*/
export function MissingEntityHelpText( { type, kind } ) {
const entityType = getEntityTypeName( type, kind );
return sprintf(
/* translators: %s is the entity type (e.g., "page", "post", "category") */
__( 'Synced %s is missing. Please update or remove this link.' ),
entityType
);
}