diff --git a/packages/block-editor/src/hooks/block-fields/index.js b/packages/block-editor/src/hooks/block-fields/index.js
index 15e5451d338d1a..dc0c514440d770 100644
--- a/packages/block-editor/src/hooks/block-fields/index.js
+++ b/packages/block-editor/src/hooks/block-fields/index.js
@@ -39,135 +39,20 @@ const CONTROLS = {
* 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)
+ * @param {Component} ControlComponent The React component for the control.
+ * @param {string} type The type of control.
+ * @param {Object} config The control configuration passed as a prop.
+ *
* @return {Function} A wrapped control component
*/
-function createConfiguredControl( config ) {
- const { control, ...controlConfig } = config;
- const ControlComponent = CONTROLS[ control ];
-
+function createConfiguredControl( ControlComponent, type, config ) {
if ( ! ControlComponent ) {
- throw new Error( `Control type "${ control }" not found` );
+ throw new Error( `Control type "${ type }" not found` );
}
return function ConfiguredControl( props ) {
- return ;
- };
-}
-
-/**
- * Normalize a media value to a canonical structure.
- * Only includes properties that are present in the field's mapping (if provided).
- *
- * @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, 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;
-}
-
-/**
- * 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.
- * Only includes properties that are present in the field's mapping (if provided).
- *
- * @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, fieldDef ) {
- const defaults = {
- url: '',
- rel: '',
- linkTarget: '',
- destination: '',
+ return ;
};
-
- 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;
-}
-
-/**
- * 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;
}
/**
@@ -218,100 +103,63 @@ function BlockFields( {
}
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, defaultValues },
- hideLabelFromVision: fieldDef.id === 'content',
- // getValue and setValue handle the mapping to block attributes
- getValue: ( { item } ) => {
- if ( fieldDef.mapping ) {
- // Extract mapped properties from the block attributes
- const mappedValue = {};
- Object.entries( fieldDef.mapping ).forEach(
- ( [ key, attrKey ] ) => {
- mappedValue[ key ] = item[ attrKey ];
- }
- );
+ };
- // Normalize to canonical structure based on field type
- if ( fieldDef.type === 'media' ) {
- return normalizeMediaValue( mappedValue, fieldDef );
+ // If the field defines a `mapping`, then custom `getValue` and `setValue`
+ // implementations are provided.
+ // These functions map from the inconsistent attribute keys found on blocks
+ // to consistent keys that the field can use internally (and back again).
+ // When `mapping` isn't provided, we can use the field API's default
+ // implementation of these functions.
+ if ( fieldDef.mapping ) {
+ field.getValue = ( { item } ) => {
+ // Extract mapped properties from the block attributes
+ const mappedValue = {};
+ Object.entries( fieldDef.mapping ).forEach(
+ ( [ key, attrKey ] ) => {
+ mappedValue[ key ] = item[ attrKey ];
}
- 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
- );
+ );
+ return mappedValue;
+ };
+ field.setValue = ( { value } ) => {
+ const attributeUpdates = {};
+ Object.entries( fieldDef.mapping ).forEach(
+ ( [ key, attrKey ] ) => {
+ attributeUpdates[ attrKey ] = value[ key ];
}
-
- // 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 denormalizedValue ) {
- updates[ attrKey ] =
- denormalizedValue[ key ];
- } else {
- updates[ attrKey ] = item[ attrKey ];
- }
- }
- );
- return updates;
- }
- // For simple id-based fields, use the id as the attribute key
- return { [ fieldDef.id ]: value };
- },
- };
+ );
+ return attributeUpdates;
+ };
+ }
// Only add custom Edit component if one exists for this type
+ const ControlComponent = CONTROLS[ fieldDef.type ];
if ( ControlComponent ) {
// Use EditConfig pattern: Edit is an object with control type and config props
- field.Edit = createConfiguredControl( {
- control: fieldDef.type,
- clientId,
- fieldDef,
- } );
+ field.Edit = createConfiguredControl(
+ ControlComponent,
+ fieldDef.type,
+ {
+ clientId,
+ fieldDef,
+ }
+ );
}
return field;
} );
- }, [ blockTypeFields, blockType?.attributes, clientId ] );
+ }, [ blockTypeFields, clientId ] );
+
+ 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;
+ }
const handleToggleField = ( fieldId ) => {
setForm( ( prev ) => {
@@ -329,12 +177,6 @@ function BlockFields( {
} );
};
- 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 (
diff --git a/packages/block-editor/src/hooks/block-fields/link/index.js b/packages/block-editor/src/hooks/block-fields/link/index.js
index df71e929b5634d..dffaeac09e9cac 100644
--- a/packages/block-editor/src/hooks/block-fields/link/index.js
+++ b/packages/block-editor/src/hooks/block-fields/link/index.js
@@ -73,11 +73,6 @@ export default function Link( { data, field, onChange, config = {} } ) {
isControl: true,
} );
const { fieldDef } = config;
- const updateAttributes = ( newValue ) => {
- const mappedChanges = field.setValue( { item: data, value: newValue } );
- onChange( mappedChanges );
- };
-
const value = field.getValue( { item: data } );
const url = value?.url;
const rel = value?.rel || '';
@@ -145,30 +140,12 @@ export default function Link( { data, field, onChange, config = {} } ) {
...newValues,
} );
- // 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 );
+ onChange(
+ field.setValue( {
+ item: data,
+ value: updatedAttrs,
+ } )
+ );
} }
onRemove={ () => {
// Remove all link-related properties based on what's in the mapping
@@ -177,20 +154,17 @@ export default function Link( { data, field, onChange, config = {} } ) {
if ( fieldDef?.mapping ) {
Object.keys( fieldDef.mapping ).forEach(
( key ) => {
- if (
- key === 'href' ||
- key === 'url' ||
- key === 'rel' ||
- key === 'target' ||
- key === 'linkTarget'
- ) {
- removeValue[ key ] = undefined;
- }
+ removeValue[ key ] = undefined;
}
);
}
- updateAttributes( removeValue );
+ onChange(
+ field.setValue( {
+ item: data,
+ value: removeValue,
+ } )
+ );
} }
/>
diff --git a/packages/block-editor/src/hooks/block-fields/media/index.js b/packages/block-editor/src/hooks/block-fields/media/index.js
index c4e03966c7441c..8b75cf84ab3dde 100644
--- a/packages/block-editor/src/hooks/block-fields/media/index.js
+++ b/packages/block-editor/src/hooks/block-fields/media/index.js
@@ -24,9 +24,9 @@ import { useInspectorPopoverPlacement } from '../use-inspector-popover-placement
import { getMediaSelectKey } from '../../../store/private-keys';
import { store as blockEditorStore } from '../../../store';
-function MediaThumbnail( { data, field, attachment } ) {
- const config = field.config || {};
- const { allowedTypes = [], multiple = false } = config;
+function MediaThumbnail( { data, field, attachment, config } ) {
+ const { fieldDef } = config;
+ const { allowedTypes = [], multiple = false } = fieldDef.args || {};
if ( multiple ) {
return 'todo multiple';
@@ -53,7 +53,7 @@ function MediaThumbnail( { data, field, attachment } ) {
const value = field.getValue( { item: data } );
const url = value?.url;
- if ( url ) {
+ if ( allowedTypes[ 0 ] === 'image' && url ) {
return (

@@ -85,15 +85,8 @@ export default function Media( { data, field, onChange, config = {} } ) {
isControl: true,
} );
const value = field.getValue( { item: data } );
- const { allowedTypes = [], multiple = false } = field.config || {};
const { fieldDef } = config;
- const updateAttributes = ( newFieldValue ) => {
- const mappedChanges = field.setValue( {
- item: data,
- value: newFieldValue,
- } );
- onChange( mappedChanges );
- };
+ const { allowedTypes = [], multiple = false } = fieldDef.args || {};
// Check if featured image is supported by checking if it's in the mapping
const hasFeaturedImageSupport =
@@ -152,102 +145,49 @@ export default function Media( { data, field, onChange, config = {} } ) {
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 ] = '';
- }
+ resetValue[ key ] = undefined;
} );
}
- // 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 } );
+ onChange(
+ field.setValue( {
+ item: data,
+ value: resetValue,
+ } )
+ );
} }
{ ...( hasFeaturedImageSupport && {
useFeaturedImage: !! value?.featuredImage,
onToggleFeaturedImage: () => {
- updateAttributes( {
- ...value,
- featuredImage: ! value?.featuredImage,
- } );
+ onChange(
+ field.setValue( {
+ item: data,
+ 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';
- }
- }
-
// Build new value dynamically based on what's in the mapping
- const newValue = {};
-
- // 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;
- }
- }
- );
- }
+ const newValue = {
+ ...selectedMedia,
+ mediaType: selectedMedia.media_type,
+ };
// 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 );
+ onChange(
+ field.setValue( {
+ item: data,
+ value: newValue,
+ } )
+ );
}
} }
renderToggle={ ( buttonProps ) => (
@@ -268,6 +208,7 @@ export default function Media( { data, field, onChange, config = {} } ) {
attachment={ attachment }
field={ field }
data={ data }
+ config={ config }
/>
{
diff --git a/packages/block-editor/src/hooks/block-fields/rich-text/index.js b/packages/block-editor/src/hooks/block-fields/rich-text/index.js
index a190887108af67..b019909138963d 100644
--- a/packages/block-editor/src/hooks/block-fields/rich-text/index.js
+++ b/packages/block-editor/src/hooks/block-fields/rich-text/index.js
@@ -33,10 +33,6 @@ export default function RichTextControl( {
const attrValue = field.getValue( { item: data } );
const fieldConfig = field.config || {};
const { clientId } = config;
- const updateAttributes = ( html ) => {
- const mappedChanges = field.setValue( { item: data, value: html } );
- onChange( mappedChanges );
- };
const [ selection, setSelection ] = useState( {
start: undefined,
end: undefined,
@@ -107,7 +103,7 @@ export default function RichTextControl( {
} = useRichText( {
value: attrValue,
onChange( html, { __unstableFormats, __unstableText } ) {
- updateAttributes( html );
+ onChange( field.setValue( { item: data, value: html } ) );
Object.values( changeHandlers ).forEach( ( changeHandler ) => {
changeHandler( __unstableFormats, __unstableText );
} );
diff --git a/packages/block-library/src/cover/index.js b/packages/block-library/src/cover/index.js
index a3e2ffb093a70b..abde17fd0107f9 100644
--- a/packages/block-library/src/cover/index.js
+++ b/packages/block-library/src/cover/index.js
@@ -67,7 +67,7 @@ if ( window.__experimentalContentOnlyInspectorFields ) {
label: __( 'Background' ),
type: 'media',
mapping: {
- type: 'backgroundType',
+ mediaType: 'backgroundType',
id: 'id',
url: 'url',
alt: 'alt',
diff --git a/packages/block-library/src/media-text/index.js b/packages/block-library/src/media-text/index.js
index d50e6eaaceb1e6..45e52a805d5a1f 100644
--- a/packages/block-library/src/media-text/index.js
+++ b/packages/block-library/src/media-text/index.js
@@ -62,7 +62,7 @@ if ( window.__experimentalContentOnlyInspectorFields ) {
type: 'media',
mapping: {
id: 'mediaId',
- type: 'mediaType',
+ mediaType: 'mediaType',
url: 'mediaUrl',
link: 'mediaLink',
},