Skip to content

Code patterns: Initial experiment. #173

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 20 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<exclude-pattern>/vendor/</exclude-pattern>
<exclude-pattern>/node_modules/</exclude-pattern>
<exclude-pattern>/lang/*</exclude-pattern>
<exclude-pattern>/includes/acf-pattern-functions.php</exclude-pattern>

<!-- How to scan -->
<arg value="sp"/>
Expand Down
192 changes: 192 additions & 0 deletions assets/src/js/bindings/custom-sources.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* WordPress dependencies.
*/
import { __ } from '@wordpress/i18n';
import { registerBlockBindingsSource } from '@wordpress/blocks';
import { store as coreDataStore } from '@wordpress/core-data';

/**
* Get the value of a specific field from the ACF fields.
*
* @param {Object} fields The ACF fields object.
* @param {string} fieldName The name of the field to retrieve.
* @returns {string} The value of the specified field, or undefined if not found.
*/
const getFieldValue = ( fields, fieldName ) => fields?.acf?.[ fieldName ];

const resolveImageAttribute = ( imageObj, attribute ) => {
if ( ! imageObj ) return '';
switch ( attribute ) {
case 'id':
return imageObj.id;
case 'url':
case 'content':
return imageObj.source_url;
case 'alt':
return imageObj.alt_text || '';
case 'title':
return imageObj.title?.rendered || '';
default:
return '';
}
};

registerBlockBindingsSource( {
name: 'scf/experimental-field',
label: 'SCF Custom Fields',
getValues( { context, bindings, select } ) {
const { getEditedEntityRecord, getMedia } = select( coreDataStore );
let fields =
context?.postType && context?.postId
? getEditedEntityRecord(
'postType',
context.postType,
context.postId
)
: undefined;
const result = {};

Object.entries( bindings ).forEach(
( [ attribute, { args } = {} ] ) => {
const fieldName = args?.field;

const fieldValue = getFieldValue( fields, fieldName );
if ( typeof fieldValue === 'object' && fieldValue !== null ) {
result[ attribute ] =
( fieldValue[ attribute ] ??
( attribute === 'content' && fieldValue.url ) ) ||
'';
} else if ( typeof fieldValue === 'number' ) {
if ( attribute === 'content' ) {
result[ attribute ] = fieldValue.toString() || '';
} else {
const imageObj = getMedia( fieldValue );
result[ attribute ] = resolveImageAttribute(
imageObj,
attribute
);
}
} else {
result[ attribute ] = fieldValue || '';
}
}
);
return result;
},
async setValues( { context, bindings, dispatch, select } ) {
const { getEditedEntityRecord } = select( coreDataStore );
if ( ! bindings || ! context?.postType || ! context?.postId ) return;

const currentPost = getEditedEntityRecord(
'postType',
context.postType,
context.postId
);
const currentAcfData = currentPost?.acf || {};
const fieldsToUpdate = {};

for ( const [ attribute, binding ] of Object.entries( bindings ) ) {
const fieldName = binding?.args?.field;
const newValue = binding?.newValue;
if ( ! fieldName || newValue === undefined ) continue;
if ( ! fieldsToUpdate[ fieldName ] ) {
fieldsToUpdate[ fieldName ] = newValue;
} else if (
attribute === 'url' &&
typeof fieldsToUpdate[ fieldName ] === 'object'
) {
fieldsToUpdate[ fieldName ] = {
...fieldsToUpdate[ fieldName ],
url: newValue,
};
} else if ( attribute === 'id' && typeof newValue === 'number' ) {
fieldsToUpdate[ fieldName ] = newValue;
}
}

const allAcfFields = { ...currentAcfData, ...fieldsToUpdate };
const processedAcfData = {};

for ( const [ key, value ] of Object.entries( allAcfFields ) ) {
// Handle specific field types requiring proper type conversion
if ( value === '' ) {
// Convert empty strings to appropriate types based on field name
if (
key === 'number' ||
key.includes( '_number' ) ||
/number$/.test( key )
) {
// Number fields should be null when empty
processedAcfData[ key ] = null;
} else if ( key.includes( 'range' ) || key === 'range_type' ) {
// Range fields should be null when empty
processedAcfData[ key ] = null;
} else if ( key.includes( '_date' ) ) {
// Date fields should be null when empty
processedAcfData[ key ] = null;
} else if ( key.includes( 'email' ) || key === 'email_type' ) {
// Handle email fields
processedAcfData[ key ] = null;
} else if ( key.includes( 'url' ) || key === 'url_type' ) {
// Handle URL fields
processedAcfData[ key ] = null;
} else {
// Other fields can remain as empty strings
processedAcfData[ key ] = value;
}
} else if ( value === 0 || value ) {
// Non-empty values - ensure numbers are actually numbers
if (
( key === 'number' ||
key.includes( '_number' ) ||
/number$/.test( key ) ) &&
value !== null
) {
// Convert string numbers to actual numbers if needed
const numValue = parseFloat( value );
processedAcfData[ key ] = isNaN( numValue )
? null
: numValue;
} else {
processedAcfData[ key ] = value;
}
} else {
// null, undefined, etc.
processedAcfData[ key ] = value;
}
}

dispatch( coreDataStore ).editEntityRecord(
'postType',
context.postType,
context.postId,
{
acf: processedAcfData,
meta: { _acf_changed: 1 },
}
);
},
canUserEditValue( { select, context, args } ) {
// Lock editing in query loop.
if ( context?.query || context?.queryId ) {
return false;
}

// Lock editing when `postType` is not defined.
if ( ! context?.postType ) {
return false;
}

// Check that the user has the capability to edit post meta.
const canUserEdit = select( coreDataStore ).canUser( 'update', {
kind: 'postType',
name: context?.postType,
id: context?.postId,
} );
if ( ! canUserEdit ) {
return false;
}

return true;
},
} );
1 change: 1 addition & 0 deletions assets/src/js/bindings/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './custom-sources.js';
66 changes: 65 additions & 1 deletion includes/Blocks/Bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,78 @@ public function __construct() {
* Hooked to acf/init, register our binding sources.
*/
public function register_binding_sources() {
if ( acf_get_setting( 'enable_block_bindings' ) ) {
register_block_bindings_source(
'acf/field',
array(
'label' => _x( 'SCF Fields', 'The core SCF block binding source name for fields on the current page', 'secure-custom-fields' ),
'get_value_callback' => array( $this, 'get_value' ),
)
);
register_block_bindings_source(
'scf/experimental-field',
array(
'label' => _x( 'SCF Fields', 'The core SCF block binding source name for fields on the current page', 'secure-custom-fields' ),
'uses_context' => array( 'postId', 'postType' ),
'get_value_callback' => array( $this, 'scf_get_block_binding_value' ),
)
);
}

/**
* Handle returning the block binding value for an ACF meta value.
*
* @since SCF 6.5
*
* @param array $source_attrs An array of the source attributes requested.
* @param \WP_Block $block_instance The block instance.
* @param string $attribute_name The block's bound attribute name.
* @return string|null The block binding value or an empty string on failure.
*/
public function scf_get_block_binding_value( $source_attrs, $block_instance, $attribute_name ) {
$post_id = $block_instance->context['postId'] ?? get_the_ID();

// Ensure we're using the parent post ID if this is a revision
if ( $post_id && wp_is_post_revision( $post_id ) ) {
$post_id = wp_get_post_parent_id( $post_id );
}

$field_name = $source_attrs['field'] ?? '';

if ( ! $post_id || ! $field_name ) {
return '';
}

$value = get_field( $field_name, $post_id );
// Handle different field types based on attribute
switch ( $attribute_name ) {
case 'content':
return is_array( $value ) ? ( $value['alt'] ?? '' ) : (string) $value;
case 'url':
if ( is_array( $value ) && isset( $value['url'] ) ) {
return $value['url'];
}
if ( is_numeric( $value ) ) {
return wp_get_attachment_url( $value );
}
return (string) $value;
case 'alt':
if ( is_array( $value ) && isset( $value['alt'] ) ) {
return $value['alt'];
}
if ( is_numeric( $value ) ) {
return get_post_meta( $value, '_wp_attachment_image_alt', true );
}
return '';
case 'id':
if ( is_array( $value ) && isset( $value['id'] ) ) {
return (string) $value['id'];
}
if ( is_numeric( $value ) ) {
return (string) $value;
}
return '';
default:
return is_string( $value ) ? $value : '';
}
}

Expand Down
5 changes: 5 additions & 0 deletions includes/acf-form-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ function acf_save_post( $post_id = 0, $values = null ) {
return false;
}

// Prevent auto-save, as we do it before in custom-sources.js.
if ( get_option( 'scf_beta_feature_code_patterns_enabled' ) ) {
return false;
}

// Set form data (useful in various filters/actions).
acf_set_form_data( 'post_id', $post_id );

Expand Down
Loading
Loading