From 90fd2a2baea5c47f0a89fe80f9aef4bcf54b0fa8 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:45:23 +1100 Subject: [PATCH 01/12] ContentOnlyControls: Switch to using DataForm --- package-lock.json | 1 + packages/block-editor/package.json | 1 + .../fields-dropdown-menu.js | 46 +++ .../components/content-only-controls/index.js | 171 ++++++++---- .../content-only-controls/link/index.js | 74 ++--- .../content-only-controls/media/index.js | 263 +++++++----------- .../content-only-controls/plain-text/index.js | 49 ---- .../content-only-controls/rich-text/index.js | 71 ++--- .../content-only-controls/styles.scss | 9 + packages/block-library/src/audio/index.js | 9 +- packages/block-library/src/button/index.js | 9 +- packages/block-library/src/code/index.js | 6 +- packages/block-library/src/cover/index.js | 3 +- packages/block-library/src/details/index.js | 6 +- packages/block-library/src/file/index.js | 15 +- packages/block-library/src/heading/index.js | 6 +- packages/block-library/src/image/index.js | 18 +- packages/block-library/src/list-item/index.js | 6 +- .../block-library/src/media-text/index.js | 6 +- packages/block-library/src/more/index.js | 6 +- .../src/navigation-link/index.js | 9 +- .../src/navigation-submenu/index.js | 9 +- packages/block-library/src/paragraph/index.js | 6 +- .../block-library/src/preformatted/index.js | 6 +- packages/block-library/src/pullquote/index.js | 12 +- packages/block-library/src/search/index.js | 18 +- .../block-library/src/social-link/index.js | 9 +- packages/block-library/src/verse/index.js | 6 +- packages/block-library/src/video/index.js | 9 +- 29 files changed, 397 insertions(+), 462 deletions(-) create mode 100644 packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js delete mode 100644 packages/block-editor/src/components/content-only-controls/plain-text/index.js diff --git a/package-lock.json b/package-lock.json index 91a763b85f0c4e..1875f484fe1e5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52960,6 +52960,7 @@ "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", + "@wordpress/dataviews": "file:../dataviews", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index efa1deb09d072c..12db5a8b52f4fa 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -65,6 +65,7 @@ "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", + "@wordpress/dataviews": "file:../dataviews", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", diff --git a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js new file mode 100644 index 00000000000000..828cf404d2d630 --- /dev/null +++ b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js @@ -0,0 +1,46 @@ +/** + * WordPress dependencies + */ +import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; +import { moreVertical, check } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; + +export default function FieldsDropdownMenu( { + fields, + visibleFields, + onToggleField, +} ) { + if ( ! fields || fields.length === 0 ) { + return null; + } + + return ( + + { ( { onClose } ) => ( + + { fields.map( ( field ) => { + const isVisible = visibleFields.includes( field.id ); + return ( + { + onToggleField( field.id ); + onClose(); + } } + role="menuitemcheckbox" + icon={ isVisible ? check : null } + > + { field.label } + + ); + } ) } + + ) } + + ); +} diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js index 38024cdbbfcaf2..47f039b1156830 100644 --- a/packages/block-editor/src/components/content-only-controls/index.js +++ b/packages/block-editor/src/components/content-only-controls/index.js @@ -6,7 +6,6 @@ import { privateApis as blocksPrivateApis, } from '@wordpress/blocks'; import { - __experimentalToolsPanel as ToolsPanel, __experimentalHStack as HStack, Icon, Navigator, @@ -14,6 +13,8 @@ import { import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { arrowLeft, arrowRight } from '@wordpress/icons'; +import { DataForm } from '@wordpress/dataviews'; +import { useState, useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -23,48 +24,20 @@ import { store as blockEditorStore } from '../../store'; import BlockIcon from '../block-icon'; import useBlockDisplayTitle from '../block-title/use-block-display-title'; import useBlockDisplayInformation from '../use-block-display-information'; -import { useInspectorPopoverPlacement } from './use-inspector-popover-placement'; const { fieldsKey } = unlock( blocksPrivateApis ); +import FieldsDropdownMenu from './fields-dropdown-menu'; // controls -import PlainText from './plain-text'; import RichText from './rich-text'; import Media from './media'; import Link from './link'; -const controls = { - PlainText, - RichText, - Media, - Link, +const CONTROLS = { + richtext: RichText, + media: Media, + link: Link, }; -function BlockAttributeToolsPanelItem( { - clientId, - control, - blockType, - attributeValues, -} ) { - const { updateBlockAttributes } = useDispatch( blockEditorStore ); - const ControlComponent = controls[ control.type ]; - - if ( ! ControlComponent ) { - return null; - } - - return ( - - updateBlockAttributes( clientId, attributes ) - } - /> - ); -} - function BlockFields( { clientId } ) { const { attributes, blockType } = useSelect( ( select ) => { @@ -80,40 +53,128 @@ function BlockFields( { clientId } ) { [ clientId ] ); + const { updateBlockAttributes } = useDispatch( blockEditorStore ); const blockTitle = useBlockDisplayTitle( { clientId, context: 'list-view', } ); const blockInformation = useBlockDisplayInformation( clientId ); - const popoverPlacementProps = useInspectorPopoverPlacement(); - if ( ! blockType?.[ fieldsKey ]?.length ) { + const blockTypeFields = blockType?.[ fieldsKey ]; + + // Track visible fields + const [ visibleFields, setVisibleFields ] = useState( () => { + // Show fields that have shownByDefault: true by default + return ( + blockTypeFields + ?.filter( ( field ) => field.shownByDefault ) + .map( ( field ) => field.id ) || [] + ); + } ); + + // Build DataForm fields with proper structure + const dataFormFields = useMemo( () => { + return blockTypeFields?.map( ( fieldDef ) => { + const ControlComponent = CONTROLS[ fieldDef.type ]; + + const field = { + id: fieldDef.id, + label: fieldDef.label, + type: fieldDef.type, // Use the field's type; DataForm will use built-in or custom Edit + config: fieldDef.args || {}, + hideLabelFromVision: fieldDef.id === 'content', + // getValue and setValue handle the mapping to block attributes + getValue: ( { item } ) => { + if ( fieldDef.mapping ) { + // For complex mappings, return an object with all mapped properties + const value = {}; + Object.entries( fieldDef.mapping ).forEach( + ( [ key, attrKey ] ) => { + value[ key ] = item[ attrKey ]; + } + ); + return value; + } + // For simple id-based fields, use the id as the attribute key + return item[ fieldDef.id ]; + }, + setValue: ( { item, value } ) => { + if ( fieldDef.mapping ) { + // Build an object with all mapped attributes + const updates = {}; + Object.entries( fieldDef.mapping ).forEach( + ( [ key, attrKey ] ) => { + updates[ attrKey ] = + value[ key ] !== undefined + ? value[ key ] + : item[ attrKey ]; + } + ); + return updates; + } + // For simple id-based fields, use the id as the attribute key + return { [ fieldDef.id ]: value }; + }, + }; + + // Only add custom Edit component if one exists for this type + if ( ControlComponent ) { + field.Edit = ControlComponent; + } + + return field; + } ); + }, [ blockTypeFields ] ); + + // Build form config showing only visible fields + const form = useMemo( + () => ( { + fields: dataFormFields + .filter( ( field ) => visibleFields.includes( field.id ) ) + .map( ( field ) => field.id ), + } ), + [ dataFormFields, visibleFields ] + ); + + const handleToggleField = ( fieldId ) => { + setVisibleFields( ( prev ) => { + if ( prev.includes( fieldId ) ) { + return prev.filter( ( id ) => id !== fieldId ); + } + return [ ...prev, fieldId ]; + } ); + }; + + if ( ! blockTypeFields?.length ) { // TODO - we might still want to show a placeholder for blocks with no fields. // for example, a way to select the block. return null; } return ( - - -
{ blockTitle }
+
+
+ + + +
{ blockTitle }
+
+
- } - panelId={ clientId } - dropdownMenuProps={ popoverPlacementProps } - > - { blockType?.[ fieldsKey ]?.map( ( field, index ) => ( - - ) ) } - +
+ { + updateBlockAttributes( clientId, changes ); + } } + /> +
); } diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js index 356f1102bbc867..d3efa0ddb52380 100644 --- a/packages/block-editor/src/components/content-only-controls/link/index.js +++ b/packages/block-editor/src/components/content-only-controls/link/index.js @@ -4,7 +4,6 @@ import { Button, Icon, - __experimentalToolsPanelItem as ToolsPanelItem, __experimentalGrid as Grid, Popover, } from '@wordpress/components'; @@ -68,35 +67,16 @@ export function getUpdatedLinkAttributes( { }; } -export default function Link( { - clientId, - control, - blockType, - attributeValues, - updateAttributes, -} ) { +export default function Link( { data, field, onChange } ) { const [ isLinkControlOpen, setIsLinkControlOpen ] = useState( false ); const { popoverProps } = useInspectorPopoverPlacement( { isControl: true, } ); - const hrefKey = control.mapping.href; - const relKey = control.mapping.rel; - const targetKey = control.mapping.target; - const destinationKey = control.mapping.destination; - - const href = attributeValues[ hrefKey ]; - const rel = attributeValues[ relKey ]; - const target = attributeValues[ targetKey ]; - const destination = attributeValues[ destinationKey ]; - const hrefDefaultValue = - blockType.attributes[ href ]?.defaultValue ?? undefined; - const relDefaultValue = - blockType.attributes[ rel ]?.defaultValue ?? undefined; - const targetDefaultValue = - blockType.attributes[ target ]?.defaultValue ?? undefined; - const destinationDefaultValue = - blockType.attributes[ destination ]?.defaultValue ?? undefined; + const value = field.getValue( { item: data } ); + const href = value?.href || value?.url; + const rel = value?.rel || ''; + const target = value?.target || value?.linkTarget; const opensInNewTab = target === NEW_TAB_TARGET; const nofollow = rel === NOFOLLOW_REL; @@ -109,20 +89,7 @@ export default function Link( { ); return ( - !! href } - onDeselect={ () => { - updateAttributes( { - [ hrefKey ]: hrefDefaultValue, - [ relKey ]: relDefaultValue, - [ targetKey ]: targetDefaultValue, - [ destinationKey ]: destinationDefaultValue, - } ); - } } - isShownByDefault={ control.shownByDefault } - > + <> - ) } - /> - + { src && ( + <> + + + { + // TODO - truncate long titles or url smartly (e.g. show filename). + attachment?.title?.raw && + attachment?.title?.raw !== '' + ? attachment?.title?.raw + : src + } + + + ) } + { ! src && ( + <> + + + { chooseItemLabel } + + + ) } + + + ) } + /> ); } diff --git a/packages/block-editor/src/components/content-only-controls/plain-text/index.js b/packages/block-editor/src/components/content-only-controls/plain-text/index.js deleted file mode 100644 index 8281c4a8e5f153..00000000000000 --- a/packages/block-editor/src/components/content-only-controls/plain-text/index.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * WordPress dependencies - */ -import { - __experimentalToolsPanelItem as ToolsPanelItem, - TextControl, -} from '@wordpress/components'; -import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; - -export default function PlainText( { - clientId, - control, - blockType, - attributeValues, - updateAttributes, -} ) { - const valueKey = control.mapping.value; - const value = attributeValues[ valueKey ]; - const defaultValue = - blockType.attributes[ valueKey ]?.defaultValue ?? undefined; - - return ( - { - return ( - value !== defaultValue && stripHTML( value )?.length !== 0 - ); - } } - onDeselect={ () => { - updateAttributes( { [ valueKey ]: defaultValue } ); - } } - isShownByDefault={ control.shownByDefault } - > - { - updateAttributes( { [ valueKey ]: newValue } ); - } } - autoComplete="off" - hideLabelFromVision={ control.shownByDefault } - /> - - ); -} diff --git a/packages/block-editor/src/components/content-only-controls/rich-text/index.js b/packages/block-editor/src/components/content-only-controls/rich-text/index.js index 559641874c2e09..21558ba2b75da9 100644 --- a/packages/block-editor/src/components/content-only-controls/rich-text/index.js +++ b/packages/block-editor/src/components/content-only-controls/rich-text/index.js @@ -1,17 +1,12 @@ /** * WordPress dependencies */ -import { - BaseControl, - useBaseControlProps, - __experimentalToolsPanelItem as ToolsPanelItem, -} from '@wordpress/components'; +import { BaseControl, useBaseControlProps } from '@wordpress/components'; import { useMergeRefs } from '@wordpress/compose'; import { useRegistry } from '@wordpress/data'; import { useRef, useState } from '@wordpress/element'; import { __unstableUseRichText as useRichText, - isEmpty, removeFormat, } from '@wordpress/rich-text'; @@ -25,17 +20,14 @@ import FormatEdit from '../../rich-text/format-edit'; import { keyboardShortcutContext, inputEventContext } from '../../rich-text'; export default function RichTextControl( { - clientId, - control, - blockType, - attributeValues, - updateAttributes, + data, + field, + onChange, + hideLabelFromVision, + config, } ) { const registry = useRegistry(); - const valueKey = control.mapping.value; - const attrValue = attributeValues[ valueKey ]; - const defaultValue = - blockType.attributes[ valueKey ]?.defaultValue ?? undefined; + const attrValue = field.getValue( { item: data } ); const [ selection, setSelection ] = useState( { start: undefined, end: undefined, @@ -46,8 +38,8 @@ export default function RichTextControl( { const keyboardShortcuts = useRef( new Set() ); const adjustedAllowedFormats = getAllowedFormats( { - allowedFormats: control.args?.allowedFormats, - disableFormats: control.args?.disableFormats, + allowedFormats: config?.allowedFormats, + disableFormats: config?.disableFormats, } ); const { @@ -57,11 +49,10 @@ export default function RichTextControl( { changeHandlers, dependencies, } = useFormatTypes( { - clientId, - identifier: valueKey, + clientId: undefined, + identifier: field.id, allowedFormats: adjustedAllowedFormats, - withoutInteractiveFormatting: - control.args?.withoutInteractiveFormatting, + withoutInteractiveFormatting: config?.withoutInteractiveFormatting, disableNoneEssentialFormatting: true, } ); @@ -102,12 +93,12 @@ export default function RichTextControl( { const { value, getValue, - onChange, + onChange: onRichTextChange, ref: richTextRef, } = useRichText( { value: attrValue, onChange( html, { __unstableFormats, __unstableText } ) { - updateAttributes( { [ valueKey ]: html } ); + onChange( field.setValue( { item: data, value: html } ) ); Object.values( changeHandlers ).forEach( ( changeHandler ) => { changeHandler( __unstableFormats, __unstableText ); } ); @@ -116,9 +107,9 @@ export default function RichTextControl( { selectionEnd: selection.end, onSelectionChange: ( start, end ) => setSelection( { start, end } ), __unstableIsSelected: isSelected, - preserveWhiteSpace: !! control.args?.preserveWhiteSpace, - placeholder: control.args?.placeholder, - __unstableDisableFormats: control.args?.disableFormats, + preserveWhiteSpace: !! config?.preserveWhiteSpace, + placeholder: config?.placeholder, + __unstableDisableFormats: config?.disableFormats, __unstableDependencies: dependencies, __unstableAfterParse: addEditorOnlyFormats, __unstableBeforeSerialize: removeEditorOnlyFormats, @@ -126,29 +117,19 @@ export default function RichTextControl( { } ); const { baseControlProps, controlProps } = useBaseControlProps( { - hideLabelFromVision: control.shownByDefault, - label: control.label, + hideLabelFromVision: hideLabelFromVision ?? field.hideLabelFromVision, + label: field.label, } ); return ( - { - return value?.text && ! isEmpty( value ); - } } - onDeselect={ () => - updateAttributes( { [ valueKey ]: defaultValue } ) - } - isShownByDefault={ control.shownByDefault } - > + <> { isSelected && (
- + ); } diff --git a/packages/block-editor/src/components/content-only-controls/styles.scss b/packages/block-editor/src/components/content-only-controls/styles.scss index 71d76cdc25a8aa..1bfadb7382dfeb 100644 --- a/packages/block-editor/src/components/content-only-controls/styles.scss +++ b/packages/block-editor/src/components/content-only-controls/styles.scss @@ -33,3 +33,12 @@ .block-editor-content-only-controls__drill-down-button { width: 100%; } + +.block-editor-content-only-controls__fields-container { + padding: 0 $grid-unit-20; +} + +.block-editor-content-only-controls__fields-header { + padding: $grid-unit-10 0; + margin-bottom: $grid-unit-05; +} diff --git a/packages/block-library/src/audio/index.js b/packages/block-library/src/audio/index.js index d00aba880fbce5..1d051918a621c7 100644 --- a/packages/block-library/src/audio/index.js +++ b/packages/block-library/src/audio/index.js @@ -39,8 +39,9 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'audio', label: __( 'Audio' ), - type: 'Media', + type: 'media', shownByDefault: true, mapping: { id: 'id', @@ -52,12 +53,10 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { }, }, { + id: 'caption', label: __( 'Caption' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'caption', - }, }, ]; } diff --git a/packages/block-library/src/button/index.js b/packages/block-library/src/button/index.js index 9425b0797d1527..090e8ff2d0ceb5 100644 --- a/packages/block-library/src/button/index.js +++ b/packages/block-library/src/button/index.js @@ -41,16 +41,15 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'text', label: __( 'Content' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'text', - }, }, { + id: 'link', label: __( 'Link' ), - type: 'Link', + type: 'link', shownByDefault: false, mapping: { href: 'url', diff --git a/packages/block-library/src/code/index.js b/packages/block-library/src/code/index.js index 072d6510a8aec9..9ab8fea1ba3cfe 100644 --- a/packages/block-library/src/code/index.js +++ b/packages/block-library/src/code/index.js @@ -46,12 +46,10 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'content', label: __( 'Code' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'content', - }, }, ]; } diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js index b5bce0823628f9..2445a7adc5a3a5 100644 --- a/packages/block-library/src/cover/index.js +++ b/packages/block-library/src/cover/index.js @@ -59,8 +59,9 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'background', label: __( 'Background' ), - type: 'Media', + type: 'media', shownByDefault: true, mapping: { type: 'backgroundType', diff --git a/packages/block-library/src/details/index.js b/packages/block-library/src/details/index.js index d19580bbc13453..9fc72c714543c2 100644 --- a/packages/block-library/src/details/index.js +++ b/packages/block-library/src/details/index.js @@ -68,12 +68,10 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'summary', label: __( 'Summary' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'summary', - }, }, ]; } diff --git a/packages/block-library/src/file/index.js b/packages/block-library/src/file/index.js index 4d05bf27d9fb73..4b007dde8685cd 100644 --- a/packages/block-library/src/file/index.js +++ b/packages/block-library/src/file/index.js @@ -39,8 +39,9 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'file', label: __( 'File' ), - type: 'Media', + type: 'media', shownByDefault: true, mapping: { id: 'id', @@ -52,20 +53,16 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { }, }, { + id: 'fileName', label: __( 'Filename' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'fileName', - }, }, { + id: 'downloadButtonText', label: __( 'Button Text' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'downloadButtonText', - }, }, ]; } diff --git a/packages/block-library/src/heading/index.js b/packages/block-library/src/heading/index.js index e4121a1e998719..3d6cde988ce78e 100644 --- a/packages/block-library/src/heading/index.js +++ b/packages/block-library/src/heading/index.js @@ -76,12 +76,10 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'content', label: __( 'Content' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'content', - }, }, ]; } diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js index 11689cb7ea6a4a..ab524f6a3ab935 100644 --- a/packages/block-library/src/image/index.js +++ b/packages/block-library/src/image/index.js @@ -69,8 +69,9 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'image', label: __( 'Image' ), - type: 'Media', + type: 'media', shownByDefault: true, mapping: { id: 'id', @@ -84,8 +85,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { }, }, { + id: 'link', label: __( 'Link' ), - type: 'Link', + type: 'link', shownByDefault: false, mapping: { href: 'href', @@ -95,20 +97,16 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { }, }, { + id: 'caption', label: __( 'Caption' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'caption', - }, }, { + id: 'alt', label: __( 'Alt text' ), - type: 'PlainText', + type: 'text', shownByDefault: false, - mapping: { - value: 'alt', - }, }, ]; } diff --git a/packages/block-library/src/list-item/index.js b/packages/block-library/src/list-item/index.js index 2be2a1f822c5f7..019ccb0a9f9dd0 100644 --- a/packages/block-library/src/list-item/index.js +++ b/packages/block-library/src/list-item/index.js @@ -39,12 +39,10 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'content', label: __( 'Content' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'content', - }, }, ]; } diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js index 66aca5edcba5c9..d3771c78692cd1 100644 --- a/packages/block-library/src/media-text/index.js +++ b/packages/block-library/src/media-text/index.js @@ -57,8 +57,9 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'media', label: __( 'Media' ), - type: 'Media', + type: 'media', shownByDefault: true, mapping: { id: 'mediaId', @@ -71,8 +72,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { }, }, { + id: 'link', label: __( 'Link' ), - type: 'Link', + type: 'link', shownByDefault: false, mapping: { href: 'href', diff --git a/packages/block-library/src/more/index.js b/packages/block-library/src/more/index.js index 1adbe3bf807bd7..5901dc2b4e462c 100644 --- a/packages/block-library/src/more/index.js +++ b/packages/block-library/src/more/index.js @@ -43,12 +43,10 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'customText', label: __( 'Content' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'customText', - }, }, ]; } diff --git a/packages/block-library/src/navigation-link/index.js b/packages/block-library/src/navigation-link/index.js index 4807bb50b2f67a..89d49d4d05cc02 100644 --- a/packages/block-library/src/navigation-link/index.js +++ b/packages/block-library/src/navigation-link/index.js @@ -96,16 +96,15 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'label', label: __( 'Label' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'label', - }, }, { + id: 'link', label: __( 'Link' ), - type: 'Link', + type: 'link', shownByDefault: false, mapping: { href: 'url', diff --git a/packages/block-library/src/navigation-submenu/index.js b/packages/block-library/src/navigation-submenu/index.js index fa0b625e174570..d6762303d5037a 100644 --- a/packages/block-library/src/navigation-submenu/index.js +++ b/packages/block-library/src/navigation-submenu/index.js @@ -55,16 +55,15 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'label', label: __( 'Label' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'label', - }, }, { + id: 'link', label: __( 'Link' ), - type: 'Link', + type: 'link', shownByDefault: false, mapping: { href: 'url', diff --git a/packages/block-library/src/paragraph/index.js b/packages/block-library/src/paragraph/index.js index 23be29edd4591c..ba14f994f5194a 100644 --- a/packages/block-library/src/paragraph/index.js +++ b/packages/block-library/src/paragraph/index.js @@ -65,12 +65,10 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'content', label: __( 'Content' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'content', - }, }, ]; } diff --git a/packages/block-library/src/preformatted/index.js b/packages/block-library/src/preformatted/index.js index ad250232e774c6..5860d00b57ae21 100644 --- a/packages/block-library/src/preformatted/index.js +++ b/packages/block-library/src/preformatted/index.js @@ -46,12 +46,10 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'content', label: __( 'Content' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'content', - }, }, ]; } diff --git a/packages/block-library/src/pullquote/index.js b/packages/block-library/src/pullquote/index.js index 697fdbdf2f7793..2bcb196b0220bd 100644 --- a/packages/block-library/src/pullquote/index.js +++ b/packages/block-library/src/pullquote/index.js @@ -43,20 +43,16 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'value', label: __( 'Content' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'value', - }, }, { + id: 'citation', label: __( 'Citation' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'citation', - }, }, ]; } diff --git a/packages/block-library/src/search/index.js b/packages/block-library/src/search/index.js index 96a738570fcf39..1dde7dd8703fc5 100644 --- a/packages/block-library/src/search/index.js +++ b/packages/block-library/src/search/index.js @@ -33,28 +33,22 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'label', label: __( 'Label' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'label', - }, }, { + id: 'buttonText', label: __( 'Button text' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'buttonText', - }, }, { + id: 'placeholder', label: __( 'Placeholder' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'placeholder', - }, }, ]; } diff --git a/packages/block-library/src/social-link/index.js b/packages/block-library/src/social-link/index.js index 28151b78ca74a5..11da16da0a0d25 100644 --- a/packages/block-library/src/social-link/index.js +++ b/packages/block-library/src/social-link/index.js @@ -29,8 +29,9 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'link', label: __( 'Link' ), - type: 'Link', + type: 'link', shownByDefault: true, mapping: { href: 'url', @@ -38,12 +39,10 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { }, }, { + id: 'label', label: __( 'Label' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'label', - }, }, ]; } diff --git a/packages/block-library/src/verse/index.js b/packages/block-library/src/verse/index.js index ea8faa5cad7d8e..5288cf821413b3 100644 --- a/packages/block-library/src/verse/index.js +++ b/packages/block-library/src/verse/index.js @@ -48,12 +48,10 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'content', label: __( 'Content' ), - type: 'RichText', + type: 'richtext', shownByDefault: true, - mapping: { - value: 'content', - }, }, ]; } diff --git a/packages/block-library/src/video/index.js b/packages/block-library/src/video/index.js index bdfea021016192..e29989051d50d7 100644 --- a/packages/block-library/src/video/index.js +++ b/packages/block-library/src/video/index.js @@ -40,8 +40,9 @@ export const settings = { if ( window.__experimentalContentOnlyPatternInsertion ) { settings[ fieldsKey ] = [ { + id: 'video', label: __( 'Video' ), - type: 'Media', + type: 'media', shownByDefault: true, mapping: { id: 'id', @@ -55,12 +56,10 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { }, }, { + id: 'caption', label: __( 'Caption' ), - type: 'RichText', + type: 'richtext', shownByDefault: false, - mapping: { - value: 'caption', - }, }, ]; } From e8c1ca477e0d9db28d078cd04f65c4e27765b0d6 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:48:12 +1100 Subject: [PATCH 02/12] Fix media with Cover, Media and Text, and Image blocks --- .../components/content-only-controls/index.js | 62 +++++++++++++-- .../content-only-controls/link/index.js | 34 ++++---- .../content-only-controls/media/index.js | 77 ++++++++++++++----- .../content-only-controls/rich-text/index.js | 12 ++- .../block-library/src/media-text/index.js | 1 + 5 files changed, 138 insertions(+), 48 deletions(-) diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js index 47f039b1156830..2d66b2f1fb19e6 100644 --- a/packages/block-editor/src/components/content-only-controls/index.js +++ b/packages/block-editor/src/components/content-only-controls/index.js @@ -74,14 +74,29 @@ function BlockFields( { clientId } ) { // Build DataForm fields with proper structure const dataFormFields = useMemo( () => { - return blockTypeFields?.map( ( fieldDef ) => { + if ( ! blockTypeFields?.length ) { + return []; + } + + return blockTypeFields.map( ( fieldDef ) => { const ControlComponent = CONTROLS[ fieldDef.type ]; + const defaultValues = {}; + if ( fieldDef.mapping && blockType?.attributes ) { + Object.entries( fieldDef.mapping ).forEach( + ( [ key, attrKey ] ) => { + defaultValues[ key ] = + blockType.attributes[ attrKey ]?.defaultValue ?? + undefined; + } + ); + } + const field = { id: fieldDef.id, label: fieldDef.label, type: fieldDef.type, // Use the field's type; DataForm will use built-in or custom Edit - config: fieldDef.args || {}, + config: { ...fieldDef.args, defaultValues }, hideLabelFromVision: fieldDef.id === 'content', // getValue and setValue handle the mapping to block attributes getValue: ( { item } ) => { @@ -104,10 +119,13 @@ function BlockFields( { clientId } ) { const updates = {}; Object.entries( fieldDef.mapping ).forEach( ( [ key, attrKey ] ) => { - updates[ attrKey ] = - value[ key ] !== undefined - ? value[ key ] - : item[ attrKey ]; + // If key is explicitly in value, use it (even if undefined to allow clearing) + // Otherwise, preserve the old value + if ( key in value ) { + updates[ attrKey ] = value[ key ]; + } else { + updates[ attrKey ] = item[ attrKey ]; + } } ); return updates; @@ -120,11 +138,20 @@ function BlockFields( { clientId } ) { // Only add custom Edit component if one exists for this type if ( ControlComponent ) { field.Edit = ControlComponent; + // Pass clientId and updateBlockAttributes to custom Edit components + field.clientId = clientId; + field.updateBlockAttributes = updateBlockAttributes; + field.fieldDef = fieldDef; } return field; } ); - }, [ blockTypeFields ] ); + }, [ + blockTypeFields, + blockType?.attributes, + clientId, + updateBlockAttributes, + ] ); // Build form config showing only visible fields const form = useMemo( @@ -171,7 +198,26 @@ function BlockFields( { clientId } ) { fields={ dataFormFields } form={ form } onChange={ ( changes ) => { - updateBlockAttributes( clientId, changes ); + // Map field values to block attributes using field.setValue + const mappedChanges = {}; + Object.entries( changes ).forEach( + ( [ fieldId, fieldValue ] ) => { + const field = dataFormFields.find( + ( f ) => f.id === fieldId + ); + if ( field && field.setValue ) { + const updates = field.setValue( { + item: attributes, + value: fieldValue, + } ); + Object.assign( mappedChanges, updates ); + } else { + // For fields without setValue, use the value directly + mappedChanges[ fieldId ] = fieldValue; + } + } + ); + updateBlockAttributes( clientId, mappedChanges ); } } />
diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js index d3efa0ddb52380..4150a845d235c2 100644 --- a/packages/block-editor/src/components/content-only-controls/link/index.js +++ b/packages/block-editor/src/components/content-only-controls/link/index.js @@ -67,12 +67,19 @@ export function getUpdatedLinkAttributes( { }; } -export default function Link( { data, field, onChange } ) { +export default function Link( { data, field } ) { const [ isLinkControlOpen, setIsLinkControlOpen ] = useState( false ); const { popoverProps } = useInspectorPopoverPlacement( { isControl: true, } ); + // For custom Edit components, we need to call updateBlockAttributes directly + const { clientId, updateBlockAttributes } = field; + const updateAttributes = ( newValue ) => { + const mappedChanges = field.setValue( { item: data, value: newValue } ); + updateBlockAttributes( clientId, mappedChanges ); + }; + const value = field.getValue( { item: data } ); const href = value?.href || value?.url; const rel = value?.rel || ''; @@ -140,24 +147,17 @@ export default function Link( { data, field, onChange } ) { ...newValues, } ); - onChange( - field.setValue( { - item: data, - value: { - ...value, - href: updatedAttrs.url, - url: updatedAttrs.url, - rel: updatedAttrs.rel, - target: updatedAttrs.linkTarget, - linkTarget: updatedAttrs.linkTarget, - }, - } ) - ); + updateAttributes( { + ...value, + href: updatedAttrs.url, + url: updatedAttrs.url, + rel: updatedAttrs.rel, + target: updatedAttrs.linkTarget, + linkTarget: updatedAttrs.linkTarget, + } ); } } onRemove={ () => { - onChange( - field.setValue( { item: data, value: {} } ) - ); + updateAttributes( {} ); } } /> diff --git a/packages/block-editor/src/components/content-only-controls/media/index.js b/packages/block-editor/src/components/content-only-controls/media/index.js index 5353c164e9ef32..ae3790717569b4 100644 --- a/packages/block-editor/src/components/content-only-controls/media/index.js +++ b/packages/block-editor/src/components/content-only-controls/media/index.js @@ -83,12 +83,27 @@ function MediaThumbnail( { data, field, attachment } ) { return ; } -export default function Media( { data, field, onChange, config } ) { +export default function Media( { data, field } ) { const { popoverProps } = useInspectorPopoverPlacement( { isControl: true, } ); const value = field.getValue( { item: data } ); - const { allowedTypes = [], multiple = false } = config || {}; + const config = field.config || {}; + const { allowedTypes = [], multiple = false } = config; + + // For custom Edit components, we need to call updateBlockAttributes directly + const { clientId, updateBlockAttributes } = field; + const updateAttributes = ( newFieldValue ) => { + const mappedChanges = field.setValue( { + item: data, + value: newFieldValue, + } ); + updateBlockAttributes( clientId, mappedChanges ); + }; + + // Check if featured image is supported by checking if it's in the value + // Cover block uses 'featuredImage' as the field property name + const hasFeaturedImageSupport = 'featuredImage' in value; const id = value?.id; const src = value?.src || value?.url; @@ -138,30 +153,52 @@ export default function Media( { data, field, onChange, config } ) { multiple={ multiple } popoverProps={ popoverProps } onReset={ () => { - onChange( field.setValue( { item: data, value: {} } ) ); - } } - useFeaturedImage={ !! value?.useFeaturedImage } - onToggleFeaturedImage={ () => { - onChange( - field.setValue( { - item: data, - value: { - ...value, - useFeaturedImage: ! value?.useFeaturedImage, - }, - } ) - ); + // Reset to empty/cleared values + const resetValue = { + id: undefined, + src: undefined, + url: undefined, + caption: '', + alt: '', + }; + // Merge with existing value to preserve other field properties + updateAttributes( { ...value, ...resetValue } ); } } + { ...( hasFeaturedImageSupport && { + useFeaturedImage: !! value?.featuredImage, + onToggleFeaturedImage: () => { + updateAttributes( { + ...value, + featuredImage: ! value?.featuredImage, + } ); + }, + } ) } onSelect={ ( selectedMedia ) => { if ( selectedMedia.id && selectedMedia.url ) { + // Determine mediaType from MIME type, not from object type + let mediaType = 'image'; // default + if ( selectedMedia.mime_type ) { + if ( + selectedMedia.mime_type.startsWith( 'video/' ) + ) { + mediaType = 'video'; + } else if ( + selectedMedia.mime_type.startsWith( 'audio/' ) + ) { + mediaType = 'audio'; + } + } + const newValue = { id: selectedMedia.id, src: selectedMedia.url, url: selectedMedia.url, + type: mediaType, }; - if ( selectedMedia.type ) { - newValue.type = selectedMedia.type; + // Capture mediaLink + if ( selectedMedia.link ) { + newValue.link = selectedMedia.link; } if ( ! value?.caption && selectedMedia.caption ) { @@ -174,9 +211,9 @@ export default function Media( { data, field, onChange, config } ) { newValue.poster = selectedMedia.poster; } - onChange( - field.setValue( { item: data, value: newValue } ) - ); + // Merge with existing value to preserve other field properties + const finalValue = { ...value, ...newValue }; + updateAttributes( finalValue ); } } } renderToggle={ ( buttonProps ) => ( diff --git a/packages/block-editor/src/components/content-only-controls/rich-text/index.js b/packages/block-editor/src/components/content-only-controls/rich-text/index.js index 21558ba2b75da9..26a67c191fbc18 100644 --- a/packages/block-editor/src/components/content-only-controls/rich-text/index.js +++ b/packages/block-editor/src/components/content-only-controls/rich-text/index.js @@ -22,12 +22,18 @@ import { keyboardShortcutContext, inputEventContext } from '../../rich-text'; export default function RichTextControl( { data, field, - onChange, hideLabelFromVision, - config, } ) { const registry = useRegistry(); const attrValue = field.getValue( { item: data } ); + const config = field.config || {}; + + // For custom Edit components, we need to call updateBlockAttributes directly + const { clientId, updateBlockAttributes } = field; + const updateAttributes = ( html ) => { + const mappedChanges = field.setValue( { item: data, value: html } ); + updateBlockAttributes( clientId, mappedChanges ); + }; const [ selection, setSelection ] = useState( { start: undefined, end: undefined, @@ -98,7 +104,7 @@ export default function RichTextControl( { } = useRichText( { value: attrValue, onChange( html, { __unstableFormats, __unstableText } ) { - onChange( field.setValue( { item: data, value: html } ) ); + updateAttributes( html ); Object.values( changeHandlers ).forEach( ( changeHandler ) => { changeHandler( __unstableFormats, __unstableText ); } ); diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js index d3771c78692cd1..23e406c5bb7fe6 100644 --- a/packages/block-library/src/media-text/index.js +++ b/packages/block-library/src/media-text/index.js @@ -65,6 +65,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { id: 'mediaId', type: 'mediaType', src: 'mediaUrl', + link: 'mediaLink', }, args: { allowedTypes: [ 'image', 'video' ], From c13fd67c660d2fb19242e3094df42be6db1f5412 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:50:10 +1100 Subject: [PATCH 03/12] Toggle off featured image when doing other things --- .../src/components/content-only-controls/media/index.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/block-editor/src/components/content-only-controls/media/index.js b/packages/block-editor/src/components/content-only-controls/media/index.js index ae3790717569b4..653155c00cb82c 100644 --- a/packages/block-editor/src/components/content-only-controls/media/index.js +++ b/packages/block-editor/src/components/content-only-controls/media/index.js @@ -161,6 +161,10 @@ export default function Media( { data, field } ) { caption: '', alt: '', }; + // Turn off featured image when resetting + if ( hasFeaturedImageSupport ) { + resetValue.featuredImage = false; + } // Merge with existing value to preserve other field properties updateAttributes( { ...value, ...resetValue } ); } } @@ -211,6 +215,11 @@ export default function Media( { data, field } ) { newValue.poster = selectedMedia.poster; } + // Turn off featured image when manually selecting media + if ( hasFeaturedImageSupport ) { + newValue.featuredImage = false; + } + // Merge with existing value to preserve other field properties const finalValue = { ...value, ...newValue }; updateAttributes( finalValue ); From db2641af9f04fffe08ce5b38e3c8057cee2d2c4f Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 19 Nov 2025 12:20:14 +1100 Subject: [PATCH 04/12] Fix linting issue... hopefully --- packages/block-editor/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/block-editor/tsconfig.json b/packages/block-editor/tsconfig.json index 1567e62fc16f6b..1a0cd9d3e2e7cf 100644 --- a/packages/block-editor/tsconfig.json +++ b/packages/block-editor/tsconfig.json @@ -9,6 +9,7 @@ { "path": "../components" }, { "path": "../compose" }, { "path": "../data" }, + { "path": "../dataviews" }, { "path": "../date" }, { "path": "../deprecated" }, { "path": "../dom" }, From 1d591a128d8b842c0eae70ee5d800c04f5c0ab30 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:28:03 +1100 Subject: [PATCH 05/12] Fix the mapping --- .../components/content-only-controls/index.js | 119 +++++++++++++++++- .../content-only-controls/link/index.js | 55 ++++++-- .../content-only-controls/media/index.js | 73 +++++++---- 3 files changed, 206 insertions(+), 41 deletions(-) diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js index 2d66b2f1fb19e6..53793e349547b8 100644 --- a/packages/block-editor/src/components/content-only-controls/index.js +++ b/packages/block-editor/src/components/content-only-controls/index.js @@ -38,6 +38,88 @@ const CONTROLS = { link: Link, }; +/** + * Normalize a media value to a canonical structure. + * Ensures all expected properties exist, even if not in the mapping. + * + * @param {Object} value - The mapped value from the block attributes + * @return {Object} Normalized media value with all properties + */ +function normalizeMediaValue( value ) { + return { + id: value?.id ?? null, + src: value?.src ?? '', + url: value?.url ?? value?.src ?? '', // url falls back to src + caption: value?.caption ?? '', + alt: value?.alt ?? '', + type: value?.type ?? 'image', + poster: value?.poster ?? '', + featuredImage: value?.featuredImage ?? false, + link: value?.link ?? '', + }; +} + +/** + * Denormalize a media value from canonical structure back to mapped keys. + * Only includes properties that are present in the field's mapping. + * + * @param {Object} value - The normalized media value + * @param {Object} fieldDef - The field definition containing the mapping + * @return {Object} Value with only mapped properties + */ +function denormalizeMediaValue( value, fieldDef ) { + if ( ! fieldDef.mapping ) { + return value; + } + + const result = {}; + Object.entries( fieldDef.mapping ).forEach( ( [ key ] ) => { + if ( key in value ) { + result[ key ] = value[ key ]; + } + } ); + return result; +} + +/** + * Normalize a link value to a canonical structure. + * Ensures all expected properties exist, even if not in the mapping. + * + * @param {Object} value - The mapped value from the block attributes + * @return {Object} Normalized link value with all properties + */ +function normalizeLinkValue( value ) { + return { + href: value?.href ?? value?.url ?? '', + url: value?.url ?? value?.href ?? '', // url falls back to href + rel: value?.rel ?? '', + target: value?.target ?? value?.linkTarget ?? '', + linkTarget: value?.linkTarget ?? value?.target ?? '', + }; +} + +/** + * Denormalize a link value from canonical structure back to mapped keys. + * Only includes properties that are present in the field's mapping. + * + * @param {Object} value - The normalized link value + * @param {Object} fieldDef - The field definition containing the mapping + * @return {Object} Value with only mapped properties + */ +function denormalizeLinkValue( value, fieldDef ) { + if ( ! fieldDef.mapping ) { + return value; + } + + const result = {}; + Object.entries( fieldDef.mapping ).forEach( ( [ key ] ) => { + if ( key in value ) { + result[ key ] = value[ key ]; + } + } ); + return result; +} + function BlockFields( { clientId } ) { const { attributes, blockType } = useSelect( ( select ) => { @@ -101,28 +183,53 @@ function BlockFields( { clientId } ) { // getValue and setValue handle the mapping to block attributes getValue: ( { item } ) => { if ( fieldDef.mapping ) { - // For complex mappings, return an object with all mapped properties - const value = {}; + // Extract mapped properties from the block attributes + const mappedValue = {}; Object.entries( fieldDef.mapping ).forEach( ( [ key, attrKey ] ) => { - value[ key ] = item[ attrKey ]; + mappedValue[ key ] = item[ attrKey ]; } ); - return value; + + // Normalize to canonical structure based on field type + if ( fieldDef.type === 'media' ) { + return normalizeMediaValue( mappedValue, fieldDef ); + } + if ( fieldDef.type === 'link' ) { + return normalizeLinkValue( mappedValue, fieldDef ); + } + + // For other types, return as-is + return mappedValue; } // For simple id-based fields, use the id as the attribute key return item[ fieldDef.id ]; }, setValue: ( { item, value } ) => { if ( fieldDef.mapping ) { + // Denormalize from canonical structure back to mapped keys + let denormalizedValue = value; + if ( fieldDef.type === 'media' ) { + denormalizedValue = denormalizeMediaValue( + value, + fieldDef + ); + } else if ( fieldDef.type === 'link' ) { + denormalizedValue = denormalizeLinkValue( + value, + fieldDef + ); + } + // Build an object with all mapped attributes const updates = {}; Object.entries( fieldDef.mapping ).forEach( ( [ key, attrKey ] ) => { // If key is explicitly in value, use it (even if undefined to allow clearing) // Otherwise, preserve the old value - if ( key in value ) { - updates[ attrKey ] = value[ key ]; + if ( key in denormalizedValue ) { + updates[ attrKey ] = + denormalizedValue[ key ]; } else { updates[ attrKey ] = item[ attrKey ]; } diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js index 4150a845d235c2..76a4755a564a12 100644 --- a/packages/block-editor/src/components/content-only-controls/link/index.js +++ b/packages/block-editor/src/components/content-only-controls/link/index.js @@ -74,7 +74,7 @@ export default function Link( { data, field } ) { } ); // For custom Edit components, we need to call updateBlockAttributes directly - const { clientId, updateBlockAttributes } = field; + const { clientId, updateBlockAttributes, fieldDef } = field; const updateAttributes = ( newValue ) => { const mappedChanges = field.setValue( { item: data, value: newValue } ); updateBlockAttributes( clientId, mappedChanges ); @@ -147,17 +147,52 @@ export default function Link( { data, field } ) { ...newValues, } ); - updateAttributes( { - ...value, - href: updatedAttrs.url, - url: updatedAttrs.url, - rel: updatedAttrs.rel, - target: updatedAttrs.linkTarget, - linkTarget: updatedAttrs.linkTarget, - } ); + // Build update object dynamically based on what's in the mapping + const updateValue = { ...value }; + + if ( fieldDef?.mapping ) { + Object.keys( fieldDef.mapping ).forEach( + ( key ) => { + if ( key === 'href' || key === 'url' ) { + updateValue[ key ] = + updatedAttrs.url; + } else if ( key === 'rel' ) { + updateValue[ key ] = + updatedAttrs.rel; + } else if ( + key === 'target' || + key === 'linkTarget' + ) { + updateValue[ key ] = + updatedAttrs.linkTarget; + } + } + ); + } + + updateAttributes( updateValue ); } } onRemove={ () => { - updateAttributes( {} ); + // Remove all link-related properties based on what's in the mapping + const removeValue = {}; + + if ( fieldDef?.mapping ) { + Object.keys( fieldDef.mapping ).forEach( + ( key ) => { + if ( + key === 'href' || + key === 'url' || + key === 'rel' || + key === 'target' || + key === 'linkTarget' + ) { + removeValue[ key ] = undefined; + } + } + ); + } + + updateAttributes( removeValue ); } } /> diff --git a/packages/block-editor/src/components/content-only-controls/media/index.js b/packages/block-editor/src/components/content-only-controls/media/index.js index 653155c00cb82c..a47762a7a8fc45 100644 --- a/packages/block-editor/src/components/content-only-controls/media/index.js +++ b/packages/block-editor/src/components/content-only-controls/media/index.js @@ -92,7 +92,7 @@ export default function Media( { data, field } ) { const { allowedTypes = [], multiple = false } = config; // For custom Edit components, we need to call updateBlockAttributes directly - const { clientId, updateBlockAttributes } = field; + const { clientId, updateBlockAttributes, fieldDef } = field; const updateAttributes = ( newFieldValue ) => { const mappedChanges = field.setValue( { item: data, @@ -101,9 +101,9 @@ export default function Media( { data, field } ) { updateBlockAttributes( clientId, mappedChanges ); }; - // Check if featured image is supported by checking if it's in the value - // Cover block uses 'featuredImage' as the field property name - const hasFeaturedImageSupport = 'featuredImage' in value; + // Check if featured image is supported by checking if it's in the mapping + const hasFeaturedImageSupport = + fieldDef?.mapping && 'featuredImage' in fieldDef.mapping; const id = value?.id; const src = value?.src || value?.url; @@ -153,7 +153,7 @@ export default function Media( { data, field } ) { multiple={ multiple } popoverProps={ popoverProps } onReset={ () => { - // Reset to empty/cleared values + // Build reset value dynamically based on mapping const resetValue = { id: undefined, src: undefined, @@ -161,10 +161,12 @@ export default function Media( { data, field } ) { caption: '', alt: '', }; - // Turn off featured image when resetting + + // Turn off featured image when resetting (only if it's in the mapping) if ( hasFeaturedImageSupport ) { resetValue.featuredImage = false; } + // Merge with existing value to preserve other field properties updateAttributes( { ...value, ...resetValue } ); } } @@ -193,26 +195,47 @@ export default function Media( { data, field } ) { } } - const newValue = { - id: selectedMedia.id, - src: selectedMedia.url, - url: selectedMedia.url, - type: mediaType, - }; - - // Capture mediaLink - if ( selectedMedia.link ) { - newValue.link = selectedMedia.link; - } + // Build new value dynamically based on what's in the mapping + const newValue = {}; - if ( ! value?.caption && selectedMedia.caption ) { - newValue.caption = selectedMedia.caption; - } - if ( ! value?.alt && selectedMedia.alt ) { - newValue.alt = selectedMedia.alt; - } - if ( selectedMedia.poster ) { - newValue.poster = selectedMedia.poster; + // Iterate over mapping keys and set values for supported properties + if ( fieldDef?.mapping ) { + Object.keys( fieldDef.mapping ).forEach( + ( key ) => { + if ( key === 'id' ) { + newValue[ key ] = selectedMedia.id; + } else if ( + key === 'src' || + key === 'url' + ) { + newValue[ key ] = selectedMedia.url; + } else if ( key === 'type' ) { + newValue[ key ] = mediaType; + } else if ( + key === 'link' && + selectedMedia.link + ) { + newValue[ key ] = selectedMedia.link; + } else if ( + key === 'caption' && + ! value?.caption && + selectedMedia.caption + ) { + newValue[ key ] = selectedMedia.caption; + } else if ( + key === 'alt' && + ! value?.alt && + selectedMedia.alt + ) { + newValue[ key ] = selectedMedia.alt; + } else if ( + key === 'poster' && + selectedMedia.poster + ) { + newValue[ key ] = selectedMedia.poster; + } + } + ); } // Turn off featured image when manually selecting media From e32127c362aaa27125cb883523f1a522ff8e36ca Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:36:17 +1100 Subject: [PATCH 06/12] Update dropdown label --- .../components/content-only-controls/fields-dropdown-menu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js index 828cf404d2d630..f307276c19c0fb 100644 --- a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js +++ b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js @@ -17,7 +17,7 @@ export default function FieldsDropdownMenu( { return ( { ( { onClose } ) => ( From a14a4aac76212f4a94a267e64465874676652ca4 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:07:22 +1100 Subject: [PATCH 07/12] Try making things a tiny bit more consistent --- .../components/content-only-controls/index.js | 33 ++++++++++++++++--- .../content-only-controls/link/index.js | 6 ++-- .../content-only-controls/media/index.js | 9 ++--- .../content-only-controls/rich-text/index.js | 25 +++++++------- 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js index 53793e349547b8..16ed48d78d22f1 100644 --- a/packages/block-editor/src/components/content-only-controls/index.js +++ b/packages/block-editor/src/components/content-only-controls/index.js @@ -38,6 +38,27 @@ const CONTROLS = { link: Link, }; +/** + * Creates a configured control component that wraps a custom control + * and passes configuration as props. + * + * @param {Object} config - The control configuration + * @param {string} config.control - The control type (key in CONTROLS map) + * @return {Function} A wrapped control component + */ +function createConfiguredControl( config ) { + const { control, ...controlConfig } = config; + const ControlComponent = CONTROLS[ control ]; + + if ( ! ControlComponent ) { + throw new Error( `Control type "${ control }" not found` ); + } + + return function ConfiguredControl( props ) { + return ; + }; +} + /** * Normalize a media value to a canonical structure. * Ensures all expected properties exist, even if not in the mapping. @@ -244,11 +265,13 @@ function BlockFields( { clientId } ) { // Only add custom Edit component if one exists for this type if ( ControlComponent ) { - field.Edit = ControlComponent; - // Pass clientId and updateBlockAttributes to custom Edit components - field.clientId = clientId; - field.updateBlockAttributes = updateBlockAttributes; - field.fieldDef = fieldDef; + // Use EditConfig pattern: Edit is an object with control type and config props + field.Edit = createConfiguredControl( { + control: fieldDef.type, + clientId, + updateBlockAttributes, + fieldDef, + } ); } return field; diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js index 76a4755a564a12..15e57bf2bce88e 100644 --- a/packages/block-editor/src/components/content-only-controls/link/index.js +++ b/packages/block-editor/src/components/content-only-controls/link/index.js @@ -67,14 +67,12 @@ export function getUpdatedLinkAttributes( { }; } -export default function Link( { data, field } ) { +export default function Link( { data, field, config = {} } ) { const [ isLinkControlOpen, setIsLinkControlOpen ] = useState( false ); const { popoverProps } = useInspectorPopoverPlacement( { isControl: true, } ); - - // For custom Edit components, we need to call updateBlockAttributes directly - const { clientId, updateBlockAttributes, fieldDef } = field; + const { clientId, updateBlockAttributes, fieldDef } = config; const updateAttributes = ( newValue ) => { const mappedChanges = field.setValue( { item: data, value: newValue } ); updateBlockAttributes( clientId, mappedChanges ); diff --git a/packages/block-editor/src/components/content-only-controls/media/index.js b/packages/block-editor/src/components/content-only-controls/media/index.js index a47762a7a8fc45..62f41fe4d34aa9 100644 --- a/packages/block-editor/src/components/content-only-controls/media/index.js +++ b/packages/block-editor/src/components/content-only-controls/media/index.js @@ -83,16 +83,13 @@ function MediaThumbnail( { data, field, attachment } ) { return ; } -export default function Media( { data, field } ) { +export default function Media( { data, field, config = {} } ) { const { popoverProps } = useInspectorPopoverPlacement( { isControl: true, } ); const value = field.getValue( { item: data } ); - const config = field.config || {}; - const { allowedTypes = [], multiple = false } = config; - - // For custom Edit components, we need to call updateBlockAttributes directly - const { clientId, updateBlockAttributes, fieldDef } = field; + const { allowedTypes = [], multiple = false } = field.config || {}; + const { clientId, updateBlockAttributes, fieldDef } = config; const updateAttributes = ( newFieldValue ) => { const mappedChanges = field.setValue( { item: data, diff --git a/packages/block-editor/src/components/content-only-controls/rich-text/index.js b/packages/block-editor/src/components/content-only-controls/rich-text/index.js index 26a67c191fbc18..3e89e577051cfb 100644 --- a/packages/block-editor/src/components/content-only-controls/rich-text/index.js +++ b/packages/block-editor/src/components/content-only-controls/rich-text/index.js @@ -23,13 +23,12 @@ export default function RichTextControl( { data, field, hideLabelFromVision, + config = {}, } ) { const registry = useRegistry(); const attrValue = field.getValue( { item: data } ); - const config = field.config || {}; - - // For custom Edit components, we need to call updateBlockAttributes directly - const { clientId, updateBlockAttributes } = field; + const fieldConfig = field.config || {}; + const { clientId, updateBlockAttributes } = config; const updateAttributes = ( html ) => { const mappedChanges = field.setValue( { item: data, value: html } ); updateBlockAttributes( clientId, mappedChanges ); @@ -44,8 +43,8 @@ export default function RichTextControl( { const keyboardShortcuts = useRef( new Set() ); const adjustedAllowedFormats = getAllowedFormats( { - allowedFormats: config?.allowedFormats, - disableFormats: config?.disableFormats, + allowedFormats: fieldConfig?.allowedFormats, + disableFormats: fieldConfig?.disableFormats, } ); const { @@ -58,7 +57,7 @@ export default function RichTextControl( { clientId: undefined, identifier: field.id, allowedFormats: adjustedAllowedFormats, - withoutInteractiveFormatting: config?.withoutInteractiveFormatting, + withoutInteractiveFormatting: fieldConfig?.withoutInteractiveFormatting, disableNoneEssentialFormatting: true, } ); @@ -113,9 +112,9 @@ export default function RichTextControl( { selectionEnd: selection.end, onSelectionChange: ( start, end ) => setSelection( { start, end } ), __unstableIsSelected: isSelected, - preserveWhiteSpace: !! config?.preserveWhiteSpace, - placeholder: config?.placeholder, - __unstableDisableFormats: config?.disableFormats, + preserveWhiteSpace: !! fieldConfig?.preserveWhiteSpace, + placeholder: fieldConfig?.placeholder, + __unstableDisableFormats: fieldConfig?.disableFormats, __unstableDependencies: dependencies, __unstableAfterParse: addEditorOnlyFormats, __unstableBeforeSerialize: removeEditorOnlyFormats, @@ -149,7 +148,7 @@ export default function RichTextControl( {
Date: Wed, 19 Nov 2025 15:22:50 +1100 Subject: [PATCH 08/12] Revert tiny clientId change --- .../src/components/content-only-controls/rich-text/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/content-only-controls/rich-text/index.js b/packages/block-editor/src/components/content-only-controls/rich-text/index.js index 3e89e577051cfb..9c5dec389e845d 100644 --- a/packages/block-editor/src/components/content-only-controls/rich-text/index.js +++ b/packages/block-editor/src/components/content-only-controls/rich-text/index.js @@ -54,7 +54,7 @@ export default function RichTextControl( { changeHandlers, dependencies, } = useFormatTypes( { - clientId: undefined, + clientId, identifier: field.id, allowedFormats: adjustedAllowedFormats, withoutInteractiveFormatting: fieldConfig?.withoutInteractiveFormatting, From 468eadc9b1f9d3096f815bda70d0ada56ea41ec8 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:19:52 +1100 Subject: [PATCH 09/12] Tidy up properties --- .../components/content-only-controls/index.js | 19 +++++----- .../content-only-controls/link/index.js | 14 +++---- .../content-only-controls/media/index.js | 38 +++++++++++-------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js index 16ed48d78d22f1..1af10e5cb2b793 100644 --- a/packages/block-editor/src/components/content-only-controls/index.js +++ b/packages/block-editor/src/components/content-only-controls/index.js @@ -61,16 +61,16 @@ function createConfiguredControl( config ) { /** * Normalize a media value to a canonical structure. - * Ensures all expected properties exist, even if not in the mapping. + * Uses standard Gutenberg attribute names for media (e.g., "url" for images). + * Accepts either name as input but normalizes to the canonical form. * * @param {Object} value - The mapped value from the block attributes - * @return {Object} Normalized media value with all properties + * @return {Object} Normalized media value with canonical properties */ function normalizeMediaValue( value ) { return { id: value?.id ?? null, - src: value?.src ?? '', - url: value?.url ?? value?.src ?? '', // url falls back to src + url: value?.url ?? value?.src ?? '', // accepts src as fallback for HTML native elements caption: value?.caption ?? '', alt: value?.alt ?? '', type: value?.type ?? 'image', @@ -104,18 +104,17 @@ function denormalizeMediaValue( value, fieldDef ) { /** * Normalize a link value to a canonical structure. - * Ensures all expected properties exist, even if not in the mapping. + * Uses standard Gutenberg attribute names: "url" and "linkTarget". + * Accepts either name as input but normalizes to the canonical form. * * @param {Object} value - The mapped value from the block attributes - * @return {Object} Normalized link value with all properties + * @return {Object} Normalized link value with canonical properties */ function normalizeLinkValue( value ) { return { - href: value?.href ?? value?.url ?? '', - url: value?.url ?? value?.href ?? '', // url falls back to href + url: value?.url ?? value?.href ?? '', // accepts href as fallback rel: value?.rel ?? '', - target: value?.target ?? value?.linkTarget ?? '', - linkTarget: value?.linkTarget ?? value?.target ?? '', + linkTarget: value?.linkTarget ?? value?.target ?? '', // accepts target as fallback }; } diff --git a/packages/block-editor/src/components/content-only-controls/link/index.js b/packages/block-editor/src/components/content-only-controls/link/index.js index 15e57bf2bce88e..b7af677f3e4d64 100644 --- a/packages/block-editor/src/components/content-only-controls/link/index.js +++ b/packages/block-editor/src/components/content-only-controls/link/index.js @@ -79,9 +79,9 @@ export default function Link( { data, field, config = {} } ) { }; const value = field.getValue( { item: data } ); - const href = value?.href || value?.url; + const url = value?.url; const rel = value?.rel || ''; - const target = value?.target || value?.linkTarget; + const target = value?.linkTarget; const opensInNewTab = target === NEW_TAB_TARGET; const nofollow = rel === NOFOLLOW_REL; @@ -89,8 +89,8 @@ export default function Link( { data, field, config = {} } ) { // 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: href, opensInNewTab, nofollow } ), - [ href, opensInNewTab, nofollow ] + () => ( { url, opensInNewTab, nofollow } ), + [ url, opensInNewTab, nofollow ] ); return ( @@ -108,15 +108,15 @@ export default function Link( { data, field, config = {} } ) { templateColumns="24px 1fr" className="block-editor-content-only-controls__link-row" > - { href && ( + { url && ( <> - { href } + { url } ) } - { ! href && ( + { ! url && ( <> ); } @@ -103,7 +103,7 @@ export default function Media( { data, field, config = {} } ) { fieldDef?.mapping && 'featuredImage' in fieldDef.mapping; const id = value?.id; - const src = value?.src || value?.url; + const url = value?.url; const attachment = useSelect( ( select ) => { @@ -146,18 +146,26 @@ export default function Media( { data, field, config = {} } ) { className="block-editor-content-only-controls__media-replace-flow" allowedTypes={ allowedTypes } mediaId={ id } - mediaURL={ src } + mediaURL={ url } multiple={ multiple } popoverProps={ popoverProps } onReset={ () => { // Build reset value dynamically based on mapping - const resetValue = { - id: undefined, - src: undefined, - url: undefined, - caption: '', - alt: '', - }; + const resetValue = {}; + + if ( fieldDef?.mapping ) { + Object.keys( fieldDef.mapping ).forEach( ( key ) => { + if ( + key === 'id' || + key === 'src' || + key === 'url' + ) { + resetValue[ key ] = undefined; + } else if ( key === 'caption' || key === 'alt' ) { + resetValue[ key ] = ''; + } + } ); + } // Turn off featured image when resetting (only if it's in the mapping) if ( hasFeaturedImageSupport ) { @@ -257,7 +265,7 @@ export default function Media( { data, field, config = {} } ) { templateColumns="24px 1fr" className="block-editor-content-only-controls__media-row" > - { src && ( + { url && ( <> ) } - { ! src && ( + { ! url && ( <> Date: Thu, 20 Nov 2025 11:49:49 +1100 Subject: [PATCH 10/12] Try to make normalize a little more consistent with the denormalize functions --- .../components/content-only-controls/index.js | 75 +++++++++++++------ packages/block-library/src/audio/index.js | 2 +- packages/block-library/src/button/index.js | 4 +- packages/block-library/src/cover/index.js | 2 +- packages/block-library/src/file/index.js | 2 +- packages/block-library/src/image/index.js | 6 +- .../block-library/src/media-text/index.js | 6 +- packages/block-library/src/video/index.js | 2 +- 8 files changed, 66 insertions(+), 33 deletions(-) diff --git a/packages/block-editor/src/components/content-only-controls/index.js b/packages/block-editor/src/components/content-only-controls/index.js index 1af10e5cb2b793..b78eeed5b1d6a1 100644 --- a/packages/block-editor/src/components/content-only-controls/index.js +++ b/packages/block-editor/src/components/content-only-controls/index.js @@ -61,23 +61,39 @@ function createConfiguredControl( config ) { /** * Normalize a media value to a canonical structure. - * Uses standard Gutenberg attribute names for media (e.g., "url" for images). - * Accepts either name as input but normalizes to the canonical form. + * Only includes properties that are present in the field's mapping (if provided). * - * @param {Object} value - The mapped value from the block attributes + * @param {Object} value - The mapped value from the block attributes (with canonical keys) + * @param {Object} fieldDef - Optional field definition containing the mapping * @return {Object} Normalized media value with canonical properties */ -function normalizeMediaValue( value ) { - return { - id: value?.id ?? null, - url: value?.url ?? value?.src ?? '', // accepts src as fallback for HTML native elements - caption: value?.caption ?? '', - alt: value?.alt ?? '', - type: value?.type ?? 'image', - poster: value?.poster ?? '', - featuredImage: value?.featuredImage ?? false, - link: value?.link ?? '', +function normalizeMediaValue( value, fieldDef ) { + const defaults = { + id: null, + url: '', + caption: '', + alt: '', + type: 'image', + poster: '', + featuredImage: false, + link: '', }; + + const result = {}; + + // If there's a mapping, only include properties that are in it + if ( fieldDef?.mapping ) { + Object.keys( fieldDef.mapping ).forEach( ( key ) => { + result[ key ] = value?.[ key ] ?? defaults[ key ] ?? ''; + } ); + return result; + } + + // Without mapping, include all default properties + Object.keys( defaults ).forEach( ( key ) => { + result[ key ] = value?.[ key ] ?? defaults[ key ]; + } ); + return result; } /** @@ -104,18 +120,35 @@ function denormalizeMediaValue( value, fieldDef ) { /** * Normalize a link value to a canonical structure. - * Uses standard Gutenberg attribute names: "url" and "linkTarget". - * Accepts either name as input but normalizes to the canonical form. + * Only includes properties that are present in the field's mapping (if provided). * - * @param {Object} value - The mapped value from the block attributes + * @param {Object} value - The mapped value from the block attributes (with canonical keys) + * @param {Object} fieldDef - Optional field definition containing the mapping * @return {Object} Normalized link value with canonical properties */ -function normalizeLinkValue( value ) { - return { - url: value?.url ?? value?.href ?? '', // accepts href as fallback - rel: value?.rel ?? '', - linkTarget: value?.linkTarget ?? value?.target ?? '', // accepts target as fallback +function normalizeLinkValue( value, fieldDef ) { + const defaults = { + url: '', + rel: '', + linkTarget: '', + destination: '', }; + + const result = {}; + + // If there's a mapping, only include properties that are in it + if ( fieldDef?.mapping ) { + Object.keys( fieldDef.mapping ).forEach( ( key ) => { + result[ key ] = value?.[ key ] ?? defaults[ key ] ?? ''; + } ); + return result; + } + + // Without mapping, include all default properties + Object.keys( defaults ).forEach( ( key ) => { + result[ key ] = value?.[ key ] ?? defaults[ key ]; + } ); + return result; } /** diff --git a/packages/block-library/src/audio/index.js b/packages/block-library/src/audio/index.js index 1d051918a621c7..6f719b60a9675a 100644 --- a/packages/block-library/src/audio/index.js +++ b/packages/block-library/src/audio/index.js @@ -45,7 +45,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { shownByDefault: true, mapping: { id: 'id', - src: 'src', + url: 'src', }, args: { allowedTypes: [ 'audio' ], diff --git a/packages/block-library/src/button/index.js b/packages/block-library/src/button/index.js index 090e8ff2d0ceb5..0358f6cad36460 100644 --- a/packages/block-library/src/button/index.js +++ b/packages/block-library/src/button/index.js @@ -52,9 +52,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { type: 'link', shownByDefault: false, mapping: { - href: 'url', + url: 'url', rel: 'rel', - target: 'linkTarget', + linkTarget: 'linkTarget', }, }, ]; diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js index 2445a7adc5a3a5..8a7018651f4c5d 100644 --- a/packages/block-library/src/cover/index.js +++ b/packages/block-library/src/cover/index.js @@ -66,7 +66,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { mapping: { type: 'backgroundType', id: 'id', - src: 'url', + url: 'url', alt: 'alt', featuredImage: 'useFeaturedImage', }, diff --git a/packages/block-library/src/file/index.js b/packages/block-library/src/file/index.js index 4b007dde8685cd..bea2253ca9098a 100644 --- a/packages/block-library/src/file/index.js +++ b/packages/block-library/src/file/index.js @@ -45,7 +45,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { shownByDefault: true, mapping: { id: 'id', - src: 'href', + url: 'href', }, args: { allowedTypes: [], diff --git a/packages/block-library/src/image/index.js b/packages/block-library/src/image/index.js index ab524f6a3ab935..3211746553f9ff 100644 --- a/packages/block-library/src/image/index.js +++ b/packages/block-library/src/image/index.js @@ -75,7 +75,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { shownByDefault: true, mapping: { id: 'id', - src: 'url', + url: 'url', caption: 'caption', alt: 'alt', }, @@ -90,9 +90,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { type: 'link', shownByDefault: false, mapping: { - href: 'href', + url: 'href', rel: 'rel', - target: 'linkTarget', + linkTarget: 'linkTarget', destination: 'linkDestination', }, }, diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js index 23e406c5bb7fe6..7c29c7dea81794 100644 --- a/packages/block-library/src/media-text/index.js +++ b/packages/block-library/src/media-text/index.js @@ -64,7 +64,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { mapping: { id: 'mediaId', type: 'mediaType', - src: 'mediaUrl', + url: 'mediaUrl', link: 'mediaLink', }, args: { @@ -78,9 +78,9 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { type: 'link', shownByDefault: false, mapping: { - href: 'href', + url: 'href', rel: 'rel', - target: 'linkTarget', + linkTarget: 'linkTarget', }, }, ]; diff --git a/packages/block-library/src/video/index.js b/packages/block-library/src/video/index.js index e29989051d50d7..a43900a7737c47 100644 --- a/packages/block-library/src/video/index.js +++ b/packages/block-library/src/video/index.js @@ -46,7 +46,7 @@ if ( window.__experimentalContentOnlyPatternInsertion ) { shownByDefault: true, mapping: { id: 'id', - src: 'src', + url: 'src', caption: 'caption', poster: 'poster', }, From 2a822b2705c1bbe9b19ea4f8b14b9cab250232d4 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:52:01 +1100 Subject: [PATCH 11/12] Ensure there's padding for the bottom field --- .../src/components/content-only-controls/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/content-only-controls/styles.scss b/packages/block-editor/src/components/content-only-controls/styles.scss index 1bfadb7382dfeb..105dc3c0c11f1e 100644 --- a/packages/block-editor/src/components/content-only-controls/styles.scss +++ b/packages/block-editor/src/components/content-only-controls/styles.scss @@ -6,7 +6,7 @@ .block-editor-content-only-controls__screen { &.components-navigator-screen { - padding: $grid-unit-10 0 0 0; + padding: $grid-unit-10 0 $grid-unit-20 0; } // Add border for the entire content controls and remove the similar border From 79242824dba8d1ab6a0e196e146c5376fae86620 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Thu, 20 Nov 2025 11:54:15 +1100 Subject: [PATCH 12/12] Update popover positioning for the dropdown menus --- .../content-only-controls/fields-dropdown-menu.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js index f307276c19c0fb..918a633aa039fa 100644 --- a/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js +++ b/packages/block-editor/src/components/content-only-controls/fields-dropdown-menu.js @@ -5,11 +5,18 @@ import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; import { moreVertical, check } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { useInspectorPopoverPlacement } from './use-inspector-popover-placement'; + export default function FieldsDropdownMenu( { fields, visibleFields, onToggleField, } ) { + const { popoverProps } = useInspectorPopoverPlacement(); + if ( ! fields || fields.length === 0 ) { return null; } @@ -18,7 +25,7 @@ export default function FieldsDropdownMenu( { { ( { onClose } ) => (