@wordpress/block-library
Version:
Block library for the WordPress editor.
324 lines (308 loc) • 8.22 kB
JSX
import clsx from 'clsx';
import { __, sprintf } from '@wordpress/i18n';
import {
DropdownMenu,
TextControl,
ToolbarButton,
__experimentalToolsPanel as ToolsPanel,
__experimentalToolsPanelItem as ToolsPanelItem,
} from '@wordpress/components';
import {
flipHorizontal as flipHorizontalIcon,
flipVertical as flipVerticalIcon,
rotateRight,
} from '@wordpress/icons';
import {
BlockControls,
InspectorControls,
useBlockProps,
useBlockEditingMode,
__experimentalUseColorProps as useColorProps,
__experimentalUseBorderProps as useBorderProps,
__experimentalGetSpacingClassesAndStyles as useSpacingProps,
getDimensionsClassesAndStyles as useDimensionsProps,
} from '@wordpress/block-editor';
import { getBlockBindingsSource } from '@wordpress/blocks';
import { useState } from '@wordpress/element';
import { SVG, Rect, Path } from '@wordpress/primitives';
import { useSelect } from '@wordpress/data';
import { store as coreDataStore } from '@wordpress/core-data';
import { useToolsPanelDropdownMenuProps } from '../utils/hooks';
import HtmlRenderer from '../utils/html-renderer';
import { CustomInserterModal } from './components';
// Matches the icon name pattern accepted by /wp/v2/icons/<name> REST route.
const ICON_NAME_REGEX =
/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?\/[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/;
const IconPlaceholder = ( { className, style } ) => (
<SVG
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 60 60"
preserveAspectRatio="none"
fill="none"
aria-hidden="true"
className={ clsx( 'wp-block-icon__placeholder', className ) }
style={ style }
>
<Rect width="60" height="60" fill="currentColor" fillOpacity={ 0.1 } />
<Path
vectorEffect="non-scaling-stroke"
stroke="currentColor"
strokeOpacity={ 0.25 }
d="M60 60 0 0"
/>
</SVG>
);
export function Edit( { attributes, setAttributes, isSelected, context } ) {
const {
icon,
ariaLabel,
flipHorizontal,
flipVertical,
rotation,
metadata,
} = attributes;
const [ isInserterOpen, setInserterOpen ] = useState( false );
const isContentOnlyMode = useBlockEditingMode() === 'contentOnly';
const { isIconBindingReadOnly = false, iconBindingReadOnlyMessage } =
useSelect(
( select ) => {
if ( ! isSelected ) {
return {};
}
const iconBinding = metadata?.bindings?.icon;
const blockBindingsSource = getBlockBindingsSource(
iconBinding?.source
);
return {
isIconBindingReadOnly:
!! iconBinding &&
! blockBindingsSource?.canUserEditValue?.( {
select,
context,
args: iconBinding.args,
} ),
iconBindingReadOnlyMessage: blockBindingsSource?.label
? sprintf(
/* translators: %s: Label of the bindings source. */
__( 'Connected to %s' ),
blockBindingsSource.label
)
: __( 'Connected to dynamic data' ),
};
},
[ context, isSelected, metadata?.bindings?.icon ]
);
const colorProps = useColorProps( attributes );
// Only padding is applied to the inner SVG element, matching the front
// end. The margin support is serialized to the block wrapper instead.
const spacingProps = useSpacingProps( {
style: {
spacing: {
padding: attributes.style?.spacing?.padding,
},
},
} );
const borderProps = useBorderProps( attributes );
const dimensionsProps = useDimensionsProps( attributes );
const selectedIcon = useSelect(
( select ) => {
if ( typeof icon !== 'string' || ! ICON_NAME_REGEX.test( icon ) ) {
return null;
}
return select( coreDataStore ).getEntityRecord(
'root',
'icon',
icon
);
},
[ icon ]
);
const iconToDisplay = selectedIcon?.content || '';
const flipClasses = {
'is-flip-horizontal': flipHorizontal,
'is-flip-vertical': flipVertical,
};
const rotationStyle = rotation ? { rotate: `${ rotation }deg` } : {};
const iconToolbarButtonLabel = icon ? __( 'Replace' ) : __( 'Choose icon' );
const blockControls = (
<>
{ icon && (
<BlockControls group="block">
<ToolbarButton
icon={ flipHorizontalIcon }
label={ __( 'Flip horizontal' ) }
isPressed={ flipHorizontal }
onClick={ () =>
setAttributes( {
flipHorizontal: ! flipHorizontal,
} )
}
/>
<ToolbarButton
icon={ flipVerticalIcon }
label={ __( 'Flip vertical' ) }
isPressed={ flipVertical }
onClick={ () =>
setAttributes( {
flipVertical: ! flipVertical,
} )
}
/>
<ToolbarButton
icon={ rotateRight }
label={ __( 'Rotate' ) }
onClick={ () =>
setAttributes( {
rotation: ( ( rotation || 0 ) + 90 ) % 360,
} )
}
/>
</BlockControls>
) }
<BlockControls group="other">
<ToolbarButton
disabled={ isIconBindingReadOnly }
label={
isIconBindingReadOnly
? iconBindingReadOnlyMessage
: undefined
}
showTooltip={ isIconBindingReadOnly }
aria-label={
isIconBindingReadOnly
? iconToolbarButtonLabel
: undefined
}
onClick={ () => {
setInserterOpen( true );
} }
>
{ iconToolbarButtonLabel }
</ToolbarButton>
{ isContentOnlyMode && icon && (
// Add some extra controls for content attributes when content only mode is active.
// With content only mode active, the inspector is hidden, so users need another way
// to edit these attributes.
<DropdownMenu
icon=""
toggleProps={ {
as: ToolbarButton,
} }
popoverProps={ {
className: 'is-alternate',
} }
text={ __( 'Label' ) }
>
{ () => (
<TextControl
className="wp-block-icon__toolbar-content"
label={ __( 'Label' ) }
value={ ariaLabel || '' }
onChange={ ( value ) =>
setAttributes( {
ariaLabel: value,
} )
}
help={ __(
'Briefly describe the icon to help screen reader users. Leave blank for decorative icons.'
) }
/>
) }
</DropdownMenu>
) }
</BlockControls>
</>
);
const dropdownMenuProps = useToolsPanelDropdownMenuProps();
const inspectorControls = icon && (
<>
<InspectorControls group="settings">
<ToolsPanel
label={ __( 'Settings' ) }
resetAll={ () =>
setAttributes( {
ariaLabel: undefined,
} )
}
dropdownMenuProps={ dropdownMenuProps }
>
<ToolsPanelItem
label={ __( 'Label' ) }
isShownByDefault
hasValue={ () => !! ariaLabel }
onDeselect={ () =>
setAttributes( { ariaLabel: undefined } )
}
>
<TextControl
label={ __( 'Label' ) }
help={ __(
'Briefly describe the icon to help screen reader users. Leave blank for decorative icons.'
) }
value={ ariaLabel || '' }
onChange={ ( value ) =>
setAttributes( { ariaLabel: value } )
}
/>
</ToolsPanelItem>
</ToolsPanel>
</InspectorControls>
</>
);
return (
<>
{ blockControls }
{ inspectorControls }
<div { ...useBlockProps() }>
{ iconToDisplay ? (
<HtmlRenderer
html={ iconToDisplay }
wrapperProps={ {
className: clsx(
colorProps.className,
borderProps.className,
spacingProps.className,
dimensionsProps.className,
flipClasses
),
style: {
...colorProps.style,
...borderProps.style,
...spacingProps.style,
...dimensionsProps.style,
...rotationStyle,
},
} }
/>
) : (
<IconPlaceholder
className={ clsx(
borderProps.className,
spacingProps.className,
dimensionsProps.className,
flipClasses
) }
style={ {
...borderProps.style,
...spacingProps.style,
...dimensionsProps.style,
...rotationStyle,
height: 'auto',
} }
/>
) }
</div>
{ isInserterOpen && (
<CustomInserterModal
onClose={ () => setInserterOpen( false ) }
value={ attributes.icon }
onChange={ ( name ) => {
setAttributes( { icon: name } );
setInserterOpen( false );
} }
/>
) }
</>
);
}
export default Edit;