UNPKG

@wordpress/block-library

Version:
324 lines (308 loc) • 8.22 kB
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;