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', },