UNPKG

@wordpress/block-library

Version:
467 lines (437 loc) 12.1 kB
import clsx from 'clsx'; import { __, sprintf } from '@wordpress/i18n'; import { useEffect, useState, useRef, useMemo, createInterpolateElement, } from '@wordpress/element'; import { TextControl, ToolbarButton, Popover, ExternalLink, } from '@wordpress/components'; import { BlockControls, InspectorControls, RichText, useBlockProps, LinkControl, __experimentalUseBorderProps as useBorderProps, __experimentalUseColorProps as useColorProps, __experimentalGetSpacingClassesAndStyles as useSpacingProps, __experimentalGetShadowClassesAndStyles as useShadowProps, __experimentalGetDimensionsClassesAndStyles as useDimensionsProps, __experimentalGetElementClassName, store as blockEditorStore, useBlockEditingMode, getTypographyClassesAndStyles as useTypographyProps, useSettings, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { displayShortcut, isKeyboardEvent, ENTER } from '@wordpress/keycodes'; import { link, linkOff } from '@wordpress/icons'; import { createBlock, cloneBlock, getDefaultBlockName, getBlockBindingsSource, } from '@wordpress/blocks'; import { useMergeRefs, useRefEffect } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants'; import { getUpdatedLinkAttributes } from './get-updated-link-attributes'; import removeAnchorTag from '../utils/remove-anchor-tag'; import { unlock } from '../lock-unlock'; import useDeprecatedTextAlign from '../utils/deprecated-text-align-attributes'; import { getWidthClasses, isPercentageWidth } from './utils'; const { HTMLElementControl } = unlock( blockEditorPrivateApis ); const LINK_SETTINGS = [ ...LinkControl.DEFAULT_LINK_SETTINGS, { id: 'nofollow', title: __( 'Mark as nofollow' ), }, ]; function useEnter( props ) { const { replaceBlocks, selectionChange } = useDispatch( blockEditorStore ); const { getBlock, getBlockRootClientId, getBlockIndex } = useSelect( blockEditorStore ); const propsRef = useRef( props ); propsRef.current = props; return useRefEffect( ( element ) => { function onKeyDown( event ) { if ( event.defaultPrevented || event.keyCode !== ENTER ) { return; } const { content, clientId } = propsRef.current; if ( content.length ) { return; } event.preventDefault(); const topParentListBlock = getBlock( getBlockRootClientId( clientId ) ); const blockIndex = getBlockIndex( clientId ); const head = cloneBlock( { ...topParentListBlock, innerBlocks: topParentListBlock.innerBlocks.slice( 0, blockIndex ), } ); const middle = createBlock( getDefaultBlockName() ); const after = topParentListBlock.innerBlocks.slice( blockIndex + 1 ); const tail = after.length ? [ cloneBlock( { ...topParentListBlock, innerBlocks: after, } ), ] : []; replaceBlocks( topParentListBlock.clientId, [ head, middle, ...tail ], 1 ); // We manually change the selection here because we are replacing // a different block than the selected one. selectionChange( middle.clientId ); } element.addEventListener( 'keydown', onKeyDown ); return () => { element.removeEventListener( 'keydown', onKeyDown ); }; }, [] ); } function ButtonEdit( props ) { const { attributes, setAttributes, className, isSelected, onReplace, mergeBlocks, clientId, context, } = props; const { tagName, linkTarget, placeholder, rel, style, text, url, metadata, } = attributes; const width = style?.dimensions?.width; useDeprecatedTextAlign( props ); const TagName = tagName || 'a'; function onKeyDown( event ) { if ( isKeyboardEvent.primary( event, 'k' ) ) { startEditing( event ); } else if ( isKeyboardEvent.primaryShift( event, 'k' ) ) { unlink(); richTextRef.current?.focus(); } } // Use internal state instead of a ref to make sure that the component // re-renders when the popover's anchor updates. const [ popoverAnchor, setPopoverAnchor ] = useState( null ); const borderProps = useBorderProps( attributes ); const colorProps = useColorProps( attributes ); const spacingProps = useSpacingProps( attributes ); const shadowProps = useShadowProps( attributes ); const dimensionsProps = useDimensionsProps( attributes ); const ref = useRef(); const richTextRef = useRef(); const blockProps = useBlockProps( { ref: useMergeRefs( [ setPopoverAnchor, ref ] ), onKeyDown, } ); const blockEditingMode = useBlockEditingMode(); const [ isEditingURL, setIsEditingURL ] = useState( false ); const isURLSet = !! url; const opensInNewTab = linkTarget === NEW_TAB_TARGET; const nofollow = !! rel?.includes( NOFOLLOW_REL ); const isLinkTag = 'a' === TagName; const { createPageEntity, userCanCreatePages, lockUrlControls = false, } = useSelect( ( select ) => { if ( ! isSelected ) { return {}; } const _settings = select( blockEditorStore ).getSettings(); const blockBindingsSource = getBlockBindingsSource( metadata?.bindings?.url?.source ); return { createPageEntity: _settings.__experimentalCreatePageEntity, userCanCreatePages: _settings.__experimentalUserCanCreatePages, lockUrlControls: !! metadata?.bindings?.url && ! blockBindingsSource?.canUserEditValue?.( { select, context, args: metadata?.bindings?.url?.args, } ), }; }, [ context, isSelected, metadata?.bindings?.url ] ); async function handleCreate( pageTitle ) { const page = await createPageEntity( { title: pageTitle, status: 'draft', } ); return { id: page.id, type: page.type, title: page.title.rendered, url: page.link, kind: 'post-type', }; } function createButtonText( searchTerm ) { return createInterpolateElement( sprintf( /* translators: %s: search term. */ __( 'Create page: <mark>%s</mark>' ), searchTerm ), { mark: <mark /> } ); } function startEditing( event ) { event.preventDefault(); setIsEditingURL( true ); } function unlink() { setAttributes( { url: undefined, linkTarget: undefined, rel: undefined, } ); setIsEditingURL( false ); } useEffect( () => { if ( ! isSelected ) { setIsEditingURL( false ); } }, [ isSelected ] ); // Memoize link value to avoid overriding the LinkControl's internal state. // This is a temporary fix. See https://github.com/WordPress/gutenberg/issues/51256. const linkValue = useMemo( () => ( { url, opensInNewTab, nofollow } ), [ url, opensInNewTab, nofollow ] ); const useEnterRef = useEnter( { content: text, clientId } ); const mergedRef = useMergeRefs( [ useEnterRef, richTextRef ] ); const [ fluidTypographySettings, layout, dimensionSizes ] = useSettings( 'typography.fluid', 'layout', 'dimensions.dimensionSizes' ); const dimensionPresets = useMemo( () => { if ( ! dimensionSizes ) { return []; } return [ ...( dimensionSizes?.custom ?? [] ), ...( dimensionSizes?.theme ?? [] ), ...( dimensionSizes?.default ?? [] ), ]; }, [ dimensionSizes ] ); const typographyProps = useTypographyProps( attributes, { typography: { fluid: fluidTypographySettings, }, layout: { wideSize: layout?.wideSize, }, } ); // Resolve preset dimension references to their actual values. const resolvedWidth = useMemo( () => { if ( ! width ) { return undefined; } const presetPrefix = 'var:preset|dimension|'; if ( width.startsWith( presetPrefix ) ) { const slug = width.slice( presetPrefix.length ); const preset = dimensionPresets?.find( ( p ) => p.slug === slug ); return preset?.size ?? width; } return width; }, [ width, dimensionPresets ] ); const hasNonContentControls = blockEditingMode === 'default'; const hasBlockControls = hasNonContentControls || ( isLinkTag && ! lockUrlControls ); const classes = clsx( blockProps.className, getWidthClasses( resolvedWidth ) ); const widthStyle = useMemo( () => { if ( ! width ) { return {}; } if ( isPercentageWidth( resolvedWidth ) ) { return { '--wp--block-button--width': parseFloat( resolvedWidth ), }; } return dimensionsProps.style; }, [ width, resolvedWidth, dimensionsProps.style ] ); return ( <> <div { ...blockProps } className={ classes } style={ { ...blockProps.style, ...widthStyle } } > <RichText ref={ mergedRef } aria-label={ __( 'Button text' ) } placeholder={ placeholder || __( 'Add text…' ) } value={ text } onChange={ ( value ) => setAttributes( { text: removeAnchorTag( value ), } ) } withoutInteractiveFormatting className={ clsx( className, 'wp-block-button__link', colorProps.className, borderProps.className, typographyProps.className, { // For backwards compatibility add style that isn't // provided via block support. 'no-border-radius': style?.border?.radius === 0, [ `has-custom-font-size` ]: blockProps.style.fontSize, }, __experimentalGetElementClassName( 'button' ) ) } style={ { ...borderProps.style, ...colorProps.style, ...spacingProps.style, ...shadowProps.style, ...typographyProps.style, writingMode: undefined, } } onReplace={ onReplace } onMerge={ mergeBlocks } identifier="text" /> </div> { hasBlockControls && ( <BlockControls group="block"> { isLinkTag && ! lockUrlControls && ( <ToolbarButton name="link" icon={ ! isURLSet ? link : linkOff } title={ ! isURLSet ? __( 'Link' ) : __( 'Unlink' ) } shortcut={ ! isURLSet ? displayShortcut.primary( 'k' ) : displayShortcut.primaryShift( 'k' ) } onClick={ ! isURLSet ? startEditing : unlink } isActive={ isURLSet } /> ) } </BlockControls> ) } { isLinkTag && isSelected && ( isEditingURL || isURLSet ) && ! lockUrlControls && ( <Popover placement="bottom" onClose={ () => { setIsEditingURL( false ); richTextRef.current?.focus(); } } anchor={ popoverAnchor } focusOnMount={ isEditingURL ? 'firstElement' : false } __unstableSlotName="__unstable-block-tools-after" shift > <LinkControl value={ linkValue } onChange={ ( { url: newURL, opensInNewTab: newOpensInNewTab, nofollow: newNofollow, } ) => setAttributes( getUpdatedLinkAttributes( { rel, url: newURL, opensInNewTab: newOpensInNewTab, nofollow: newNofollow, } ) ) } onRemove={ () => { unlink(); richTextRef.current?.focus(); } } forceIsEditingLink={ isEditingURL } settings={ LINK_SETTINGS } createSuggestion={ createPageEntity && handleCreate } withCreateSuggestion={ userCanCreatePages } createSuggestionButtonText={ createButtonText } /> </Popover> ) } <InspectorControls group="advanced"> <HTMLElementControl tagName={ tagName } onChange={ ( value ) => setAttributes( { tagName: value } ) } options={ [ { label: __( 'Default (<a>)' ), value: 'a' }, { label: '<button>', value: 'button' }, ] } /> { isLinkTag && ( <TextControl __next40pxDefaultSize label={ __( 'Link relation' ) } help={ createInterpolateElement( __( 'The <a>Link Relation</a> attribute defines the relationship between a linked resource and the current document.' ), { a: ( <ExternalLink href="https://developer.mozilla.org/docs/Web/HTML/Attributes/rel" /> ), } ) } value={ rel || '' } onChange={ ( newRel ) => setAttributes( { rel: newRel } ) } /> ) } </InspectorControls> </> ); } export default ButtonEdit;