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 } );