diff --git a/packages/block-editor/src/components/block-edit/edit.js b/packages/block-editor/src/components/block-edit/edit.js index 83d0e3f406f82..32095bc08c11c 100644 --- a/packages/block-editor/src/components/block-edit/edit.js +++ b/packages/block-editor/src/components/block-edit/edit.js @@ -11,8 +11,14 @@ import { getBlockDefaultClassName, hasBlockSupport, getBlockType, + getBlockBindingsSource, } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; import { useContext, useMemo } from '@wordpress/element'; +/** + * Internal dependencies + */ +import { InspectorControls, BlockControls } from '../../components'; /** * Internal dependencies @@ -29,6 +35,41 @@ import BlockContext from '../block-context'; */ const DEFAULT_BLOCK_CONTEXT = {}; +const AttributeWrapper = ( { control, ...props } ) => { + const { context, isSelected, attributes } = props; + const { metadata } = attributes; + const { key } = control; + const isDisabled = useSelect( + ( select ) => { + if ( ! isSelected ) { + return {}; + } + + const blockBindingsSource = getBlockBindingsSource( + metadata?.bindings?.[ key ]?.source + ); + + return ( + !! metadata?.bindings?.[ key ] && + ! blockBindingsSource?.canUserEditValue?.( { + select, + context, + args: metadata?.bindings?.[ key ]?.args, + } ) + ); + }, + [ context, isSelected, key, metadata?.bindings ] + ); + const Wrapper = + control.type === 'toolbar' ? BlockControls : InspectorControls; + + return ( + + + + ); +}; + const Edit = ( props ) => { const { name } = props; const blockType = getBlockType( name ); @@ -37,12 +78,22 @@ const Edit = ( props ) => { return null; } + const controls = []; + + if ( blockType.attributeControls?.length > 0 ) { + for ( const control of blockType.attributeControls ) { + controls.push( + + ); + } + } + // `edit` and `save` are functions or components describing the markup // with which a block is displayed. If `blockType` is valid, assign // them preferentially as the render value for the block. const Component = blockType.edit || blockType.save; - return ; + return [ , controls ]; }; const EditWithFilters = withFilters( 'editor.BlockEdit' )( Edit ); diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 3539fd54f4eec..e96768b80ea2b 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -6,57 +6,32 @@ import clsx from 'clsx'; /** * Internal dependencies */ -import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants'; -import { getUpdatedLinkAttributes } from './get-updated-link-attributes'; import removeAnchorTag from '../utils/remove-anchor-tag'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useEffect, useState, useRef, useMemo } from '@wordpress/element'; +import { useEffect, useState, useRef } from '@wordpress/element'; import { - Button, - ButtonGroup, - PanelBody, - TextControl, - ToolbarButton, - Popover, -} from '@wordpress/components'; -import { - AlignmentControl, - BlockControls, - InspectorControls, RichText, useBlockProps, __experimentalUseBorderProps as useBorderProps, __experimentalUseColorProps as useColorProps, __experimentalGetSpacingClassesAndStyles as useSpacingProps, __experimentalGetShadowClassesAndStyles as useShadowProps, - __experimentalLinkControl as LinkControl, __experimentalGetElementClassName, store as blockEditorStore, - useBlockEditingMode, } from '@wordpress/block-editor'; -import { displayShortcut, isKeyboardEvent, ENTER } from '@wordpress/keycodes'; -import { link, linkOff } from '@wordpress/icons'; +import { isKeyboardEvent, ENTER } from '@wordpress/keycodes'; import { createBlock, cloneBlock, getDefaultBlockName, - getBlockBindingsSource, } from '@wordpress/blocks'; import { useMergeRefs, useRefEffect } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; -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 } = @@ -113,39 +88,6 @@ function useEnter( props ) { }, [] ); } -function WidthPanel( { selectedWidth, setAttributes } ) { - function handleChange( newWidth ) { - // Check if we are toggling the width off - const width = selectedWidth === newWidth ? undefined : newWidth; - - // Update attributes. - setAttributes( { width } ); - } - - return ( - - - { [ 25, 50, 75, 100 ].map( ( widthValue ) => { - return ( - - ); - } ) } - - - ); -} - function ButtonEdit( props ) { const { attributes, @@ -155,22 +97,8 @@ function ButtonEdit( props ) { onReplace, mergeBlocks, clientId, - context, } = props; - const { - tagName, - textAlign, - linkTarget, - placeholder, - rel, - style, - text, - url, - width, - metadata, - } = attributes; - - const TagName = tagName || 'a'; + const { textAlign, placeholder, style, text, width } = attributes; function onKeyDown( event ) { if ( isKeyboardEvent.primary( event, 'k' ) ) { @@ -195,13 +123,7 @@ function ButtonEdit( props ) { 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; function startEditing( event ) { event.preventDefault(); @@ -223,39 +145,9 @@ function ButtonEdit( props ) { } }, [ 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 { lockUrlControls = false } = useSelect( - ( select ) => { - if ( ! isSelected ) { - return {}; - } - - const blockBindingsSource = getBlockBindingsSource( - metadata?.bindings?.url?.source - ); - - return { - lockUrlControls: - !! metadata?.bindings?.url && - ! blockBindingsSource?.canUserEditValue?.( { - select, - context, - args: metadata?.bindings?.url?.args, - } ), - }; - }, - [ context, isSelected, metadata?.bindings?.url ] - ); - return ( <>
- - { blockEditingMode === 'default' && ( - { - setAttributes( { textAlign: nextAlign } ); - } } - /> - ) } - { ! isURLSet && isLinkTag && ! lockUrlControls && ( - - ) } - { isURLSet && isLinkTag && ! lockUrlControls && ( - - ) } - - { isLinkTag && - isSelected && - ( isEditingURL || isURLSet ) && - ! lockUrlControls && ( - { - setIsEditingURL( false ); - richTextRef.current?.focus(); - } } - anchor={ popoverAnchor } - focusOnMount={ isEditingURL ? 'firstElement' : false } - __unstableSlotName="__unstable-block-tools-after" - shift - > - - setAttributes( - getUpdatedLinkAttributes( { - rel, - url: newURL, - opensInNewTab: newOpensInNewTab, - nofollow: newNofollow, - } ) - ) - } - onRemove={ () => { - unlink(); - richTextRef.current?.focus(); - } } - forceIsEditingLink={ isEditingURL } - settings={ LINK_SETTINGS } - /> - - ) } - - - - - { isLinkTag && ( - - setAttributes( { rel: newRel } ) - } - /> - ) } - ); } diff --git a/packages/block-library/src/button/index.js b/packages/block-library/src/button/index.js index 2b05b280028ab..7ba4d8052e78c 100644 --- a/packages/block-library/src/button/index.js +++ b/packages/block-library/src/button/index.js @@ -1,18 +1,43 @@ /** * WordPress dependencies */ +import { + AlignmentControl, + __experimentalLinkControl as LinkControl, + useBlockEditingMode, +} from '@wordpress/block-editor'; +import { + Button, + ButtonGroup, + PanelBody, + TextControl, + ToolbarButton, + Popover, +} from '@wordpress/components'; +import { useEffect, useState, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { button as icon } from '@wordpress/icons'; +import { button as icon, link, linkOff } from '@wordpress/icons'; +import { displayShortcut } from '@wordpress/keycodes'; /** * Internal dependencies */ +import { NEW_TAB_TARGET, NOFOLLOW_REL } from './constants'; +import { getUpdatedLinkAttributes } from './get-updated-link-attributes'; import initBlock from '../utils/init-block'; import deprecated from './deprecated'; import edit from './edit'; import metadata from './block.json'; import save from './save'; +const LINK_SETTINGS = [ + ...LinkControl.DEFAULT_LINK_SETTINGS, + { + id: 'nofollow', + title: __( 'Mark as nofollow' ), + }, +]; + const { name } = metadata; export { metadata, name }; @@ -32,6 +57,194 @@ export const settings = { ...a, text: ( a.text || '' ) + text, } ), + attributeControls: [ + { + key: 'url', + type: 'toolbar', + group: 'block', + Control( { isSelected, isDisabled, attributes, setAttributes } ) { + const blockEditingMode = useBlockEditingMode(); + const { linkTarget, rel, tagName, textAlign, url } = attributes; + const [ isEditingURL, setIsEditingURL ] = useState( false ); + const isURLSet = !! url; + const opensInNewTab = linkTarget === NEW_TAB_TARGET; + const nofollow = !! rel?.includes( NOFOLLOW_REL ); + const TagName = tagName || 'a'; + const isLinkTag = 'a' === TagName; + + 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 ] + ); + + // 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 ); + + return ( + <> + { blockEditingMode === 'default' && ( + { + setAttributes( { textAlign: nextAlign } ); + } } + /> + ) } + { ! isURLSet && isLinkTag && ! isDisabled && ( + + ) } + { isURLSet && isLinkTag && ! isDisabled && ( + + ) } + { isLinkTag && + isSelected && + ( isEditingURL || isURLSet ) && + ! isDisabled && ( + { + setIsEditingURL( false ); + // TODO: Check how to access the richTextRef that is not defined here. + richTextRef.current?.focus(); + } } + anchor={ popoverAnchor } + focusOnMount={ + isEditingURL ? 'firstElement' : false + } + __unstableSlotName="__unstable-block-tools-after" + shift + > + + setAttributes( + getUpdatedLinkAttributes( { + rel, + url: newURL, + opensInNewTab: + newOpensInNewTab, + nofollow: newNofollow, + } ) + ) + } + onRemove={ () => { + unlink(); + richTextRef.current?.focus(); + } } + forceIsEditingLink={ isEditingURL } + settings={ LINK_SETTINGS } + /> + + ) } + + ); + }, + }, + { + key: 'width', + Control( { attributes, setAttributes } ) { + const { width: selectedWidth } = attributes; + function handleChange( newWidth ) { + // Check if we are toggling the width off + const width = + selectedWidth === newWidth ? undefined : newWidth; + + // Update attributes. + setAttributes( { width } ); + } + + return ( + + + { [ 25, 50, 75, 100 ].map( ( widthValue ) => { + return ( + + ); + } ) } + + + ); + }, + }, + { + key: 'rel', + group: 'advanced', + Control( { attributes, setAttributes } ) { + const { rel, tagName } = attributes; + const TagName = tagName || 'a'; + const isLinkTag = 'a' === TagName; + + return ( + <> + { isLinkTag && ( + + setAttributes( { rel: newRel } ) + } + /> + ) } + + ); + }, + }, + ], }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 7edfbbc6252ae..e3c386cbf3580 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -6,89 +6,21 @@ import clsx from 'clsx'; /** * WordPress dependencies */ -import { __, _x, isRTL } from '@wordpress/i18n'; -import { - ToolbarButton, - ToggleControl, - __experimentalToolsPanelItem as ToolsPanelItem, -} from '@wordpress/components'; -import { - AlignmentControl, - BlockControls, - InspectorControls, - RichText, - useBlockProps, - useSettings, - useBlockEditingMode, -} from '@wordpress/block-editor'; -import { formatLtr } from '@wordpress/icons'; +import { __, isRTL } from '@wordpress/i18n'; +import { RichText, useBlockProps } from '@wordpress/block-editor'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies */ import { useOnEnter } from './use-enter'; -function ParagraphRTLControl( { direction, setDirection } ) { - return ( - isRTL() && ( - { - setDirection( direction === 'ltr' ? undefined : 'ltr' ); - } } - /> - ) - ); -} +const name = 'core/paragraph'; function hasDropCapDisabled( align ) { return align === ( isRTL() ? 'left' : 'right' ) || align === 'center'; } -function DropCapControl( { clientId, attributes, setAttributes } ) { - // Please do not add a useSelect call to the paragraph block unconditionally. - // Every useSelect added to a (frequently used) block will degrade load - // and type performance. By moving it within InspectorControls, the subscription is - // now only added for the selected block(s). - const [ isDropCapFeatureEnabled ] = useSettings( 'typography.dropCap' ); - - if ( ! isDropCapFeatureEnabled ) { - return null; - } - - const { align, dropCap } = attributes; - - let helpText; - if ( hasDropCapDisabled( align ) ) { - helpText = __( 'Not available for aligned text.' ); - } else if ( dropCap ) { - helpText = __( 'Showing large initial letter.' ); - } else { - helpText = __( 'Toggle to show a large initial letter.' ); - } - - return ( - !! dropCap } - label={ __( 'Drop cap' ) } - onDeselect={ () => setAttributes( { dropCap: undefined } ) } - resetAllFilter={ () => ( { dropCap: undefined } ) } - panelId={ clientId } - > - setAttributes( { dropCap: ! dropCap } ) } - help={ helpText } - disabled={ hasDropCapDisabled( align ) ? true : false } - /> - - ); -} - function ParagraphBlock( { attributes, mergeBlocks, @@ -106,63 +38,49 @@ function ParagraphBlock( { } ), style: { direction }, } ); - const blockEditingMode = useBlockEditingMode(); - return ( - <> - { blockEditingMode === 'default' && ( - - - setAttributes( { - align: newAlign, - dropCap: hasDropCapDisabled( newAlign ) - ? false - : dropCap, - } ) - } - /> - - setAttributes( { direction: newDirection } ) - } - /> - - ) } - - - - - setAttributes( { content: newContent } ) + + setAttributes( { content: newContent } ) + } + onSplit={ ( value, isOriginal ) => { + let newAttributes; + + if ( isOriginal || value ) { + newAttributes = { + ...attributes, + content: value, + }; } - onMerge={ mergeBlocks } - onReplace={ onReplace } - onRemove={ onRemove } - aria-label={ - RichText.isEmpty( content ) - ? __( - 'Empty block; start writing or type forward slash to choose a block' - ) - : __( 'Block: Paragraph' ) + + const block = createBlock( name, newAttributes ); + + if ( isOriginal ) { + block.clientId = clientId; } - data-empty={ RichText.isEmpty( content ) } - placeholder={ placeholder || __( 'Type / to choose a block' ) } - data-custom-placeholder={ placeholder ? true : undefined } - __unstableEmbedURLOnPaste - __unstableAllowPrefixTransformations - /> - + + return block; + } } + onMerge={ mergeBlocks } + onReplace={ onReplace } + onRemove={ onRemove } + aria-label={ + RichText.isEmpty( content ) + ? __( + 'Empty block; start writing or type forward slash to choose a block' + ) + : __( 'Block: Paragraph' ) + } + data-empty={ RichText.isEmpty( content ) } + placeholder={ placeholder || __( 'Type / to choose a block' ) } + data-custom-placeholder={ placeholder ? true : undefined } + __unstableEmbedURLOnPaste + __unstableAllowPrefixTransformations + /> ); } diff --git a/packages/block-library/src/paragraph/index.js b/packages/block-library/src/paragraph/index.js index 715fb35ec05ab..82c5257d58bde 100644 --- a/packages/block-library/src/paragraph/index.js +++ b/packages/block-library/src/paragraph/index.js @@ -1,8 +1,14 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; -import { paragraph as icon } from '@wordpress/icons'; +import { __, _x, isRTL } from '@wordpress/i18n'; +import { paragraph as icon, formatLtr } from '@wordpress/icons'; +import { AlignmentControl, useSettings } from '@wordpress/block-editor'; +import { + ToolbarButton, + ToggleControl, + __experimentalToolsPanelItem as ToolsPanelItem, +} from '@wordpress/components'; /** * Internal dependencies @@ -18,6 +24,10 @@ const { name } = metadata; export { metadata, name }; +function hasDropCapDisabled( align ) { + return align === ( isRTL() ? 'left' : 'right' ) || align === 'center'; +} + export const settings = { icon, example: { @@ -54,6 +64,101 @@ export const settings = { }, edit, save, + attributeControls: [ + { + key: 'align', + type: 'toolbar', + group: 'block', + Control( { attributes, setAttributes } ) { + const { align, dropCap } = attributes; + return ( + + setAttributes( { + align: newAlign, + dropCap: hasDropCapDisabled( newAlign ) + ? false + : dropCap, + } ) + } + /> + ); + }, + }, + { + key: 'direction', + type: 'toolbar', + group: 'block', + Control( { attributes, setAttributes } ) { + const { direction } = attributes; + return ( + isRTL() && ( + { + setAttributes( { + direction: + direction === 'ltr' ? undefined : 'ltr', + } ); + } } + /> + ) + ); + }, + }, + { + key: 'dropCap', + type: 'inspector', + group: 'typography', + Control( { attributes, setAttributes, clientId } ) { + const [ isDropCapFeatureEnabled ] = + useSettings( 'typography.dropCap' ); + + if ( ! isDropCapFeatureEnabled ) { + return null; + } + + const { align, dropCap } = attributes; + + let helpText; + if ( hasDropCapDisabled( align ) ) { + helpText = __( 'Not available for aligned text.' ); + } else if ( dropCap ) { + helpText = __( 'Showing large initial letter.' ); + } else { + helpText = __( 'Toggle to show a large initial letter.' ); + } + + return ( + !! dropCap } + label={ __( 'Drop cap' ) } + onDeselect={ () => + setAttributes( { dropCap: undefined } ) + } + resetAllFilter={ () => ( { dropCap: undefined } ) } + panelId={ clientId } + > + + setAttributes( { dropCap: ! dropCap } ) + } + help={ helpText } + disabled={ + hasDropCapDisabled( align ) ? true : false + } + /> + + ); + }, + }, + ], }; export const init = () => initBlock( { name, metadata, settings } );