diff --git a/assets/src/js/bindings/block-editor.js b/assets/src/js/bindings/block-editor.js
new file mode 100644
index 00000000..b49aca93
--- /dev/null
+++ b/assets/src/js/bindings/block-editor.js
@@ -0,0 +1,345 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect, useCallback, useMemo } from '@wordpress/element';
+import { addFilter } from '@wordpress/hooks';
+import { createHigherOrderComponent } from '@wordpress/compose';
+import {
+ InspectorControls,
+ useBlockBindingsUtils,
+} from '@wordpress/block-editor';
+import {
+ BaseControl,
+ ComboboxControl,
+ __experimentalToolsPanel as ToolsPanel,
+ __experimentalToolsPanelItem as ToolsPanelItem,
+ __experimentalHStack as HStack,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useSelect } from '@wordpress/data';
+import { store as coreDataStore } from '@wordpress/core-data';
+import { store as editorStore } from '@wordpress/editor';
+
+/**
+ * Internal dependencies
+ */
+import BlockAttributesControlLinkedButton from './components/block-attributes-control-linked-button';
+
+// These constant and the function above have been copied from Gutenberg. It should be public, eventually.
+
+const BLOCK_BINDINGS_ALLOWED_BLOCKS = {
+ 'core/paragraph': [ 'content' ],
+ 'core/heading': [ 'content' ],
+ 'core/image': [ 'id', 'url', 'title', 'alt' ],
+ 'core/button': [ 'url', 'text', 'linkTarget', 'rel' ],
+};
+
+/**
+ * Gets the bindable attributes for a given block.
+ *
+ * @param {string} blockName The name of the block.
+ *
+ * @return {string[]} The bindable attributes for the block.
+ */
+function getBindableAttributes( blockName ) {
+ return BLOCK_BINDINGS_ALLOWED_BLOCKS[ blockName ];
+}
+
+/**
+ * Add custom controls to all blocks
+ */
+const withCustomControls = createHigherOrderComponent( ( BlockEdit ) => {
+ return ( props ) => {
+ const bindableAttributes = getBindableAttributes( props.name );
+ const showLinkedButton = props.name === 'core/image';
+ const { updateBlockBindings, removeAllBlockBindings } =
+ useBlockBindingsUtils();
+
+ const { postType, postId } = useSelect( ( select ) => {
+ const { getCurrentPostType, getCurrentPostId } =
+ select( editorStore );
+ return {
+ postType: getCurrentPostType(),
+ postId: getCurrentPostId(),
+ };
+ }, [] );
+
+ const fieldsGroups = useSelect(
+ ( select ) => {
+ const { getEditedEntityRecord } = select( coreDataStore );
+
+ if ( ! postType || ! postId ) {
+ return undefined;
+ }
+
+ const record = getEditedEntityRecord(
+ 'postType',
+ postType,
+ postId
+ );
+ return record?.scf_field_groups;
+ },
+ [ postType, postId ]
+ );
+
+ const currentBindings = props.attributes?.metadata?.bindings || {};
+
+ const fields = useMemo(
+ () =>
+ fieldsGroups?.reduce( ( acc, fieldGroup ) => {
+ const groupFields =
+ fieldGroup.fields?.map( ( field ) => ( {
+ ...field,
+ fieldGroupTitle: fieldGroup.title,
+ name: field.name,
+ label: field.label,
+ value: field.value,
+ } ) ) || [];
+
+ return [ ...acc, ...groupFields ];
+ }, [] ) || [],
+ [ fieldsGroups ]
+ );
+
+ const fieldsSuggestions = useMemo( () => {
+ if ( props.name === 'core/image' ) {
+ // return only the type image fields
+ return fields
+ .filter( ( field ) => field.type === 'image' )
+ .map( ( field ) => ( {
+ value: field.name,
+ label: field.label,
+ } ) );
+ } else {
+ return fields.map( ( field ) => ( {
+ value: field.name,
+ label: field.label,
+ } ) );
+ }
+ }, [ fields ] );
+
+ // Initialize the field state with an empty object to track multiple attributes
+ const [ boundFields, setBoundFields ] = useState( {} );
+ const [ allBoundFields, setAllBoundFields ] = useState(
+ props.name === 'core/image'
+ );
+
+ // Memoize the stringified currentBindings to avoid unnecessary effect runs
+ const currentBindingsKey = useMemo(
+ () => JSON.stringify( currentBindings ),
+ [ currentBindings ]
+ );
+
+ // Initialize bound fields from current bindings when they change
+ useEffect( () => {
+ if ( Object.keys( currentBindings ).length > 0 ) {
+ const initialBoundFields = {};
+
+ // Extract field values from current bindings
+ Object.keys( currentBindings ).forEach( ( attribute ) => {
+ if ( currentBindings[ attribute ]?.args?.key ) {
+ initialBoundFields[ attribute ] =
+ currentBindings[ attribute ].args.key;
+ }
+ } );
+
+ setBoundFields( initialBoundFields );
+ } else {
+ // Clear bound fields when there are no current bindings
+ setBoundFields( {} );
+ }
+ }, [ currentBindingsKey ] );
+
+ // Memoize the change handler to prevent creating new function on each render
+ const handleFieldChange = useCallback(
+ ( attributes, value ) => {
+ // Ensure attributes is always an array.
+ const attributeArray = Array.isArray( attributes )
+ ? attributes
+ : [ attributes ];
+
+ if ( attributeArray.length > 1 ) {
+ setBoundFields( ( prevState ) => {
+ const newState = { ...prevState };
+ const bindings = {};
+
+ attributeArray.forEach( ( attr ) => {
+ newState[ attr ] = value;
+ bindings[ attr ] = {
+ source: 'acf/field',
+ args: {
+ key: value,
+ },
+ };
+ } );
+
+ // Update all bindings at once.
+ updateBlockBindings( bindings );
+
+ return newState;
+ } );
+ } else {
+ const singleAttribute = attributeArray[ 0 ];
+ setBoundFields( ( prevState ) => ( {
+ ...prevState,
+ [ singleAttribute ]: value,
+ } ) );
+ updateBlockBindings( {
+ [ singleAttribute ]: {
+ source: 'acf/field',
+ args: {
+ key: value,
+ },
+ },
+ } );
+ }
+ },
+ [ updateBlockBindings ]
+ );
+
+ const handleReset = useCallback( () => {
+ removeAllBlockBindings();
+ setBoundFields( {} );
+ }, [ removeAllBlockBindings ] );
+
+ if ( fieldsSuggestions.length === 0 || ! bindableAttributes ) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+
+ { showLinkedButton && (
+
+
+ { allBoundFields
+ ? __(
+ 'Unlink all attributes',
+ 'secure-custom-fields'
+ )
+ : __(
+ 'Link all attributes',
+ 'secure-custom-fields'
+ ) }
+
+ {
+ setAllBoundFields( ! allBoundFields );
+ } }
+ />
+
+ ) }
+ { allBoundFields ? (
+
+ !! boundFields[ bindableAttributes[ 0 ] ]
+ }
+ label={ __(
+ 'All attributes',
+ 'secure-custom-fields'
+ ) }
+ onDeselect={ () =>
+ handleFieldChange( bindableAttributes, '' )
+ }
+ isShownByDefault={ true }
+ >
+
+ handleFieldChange(
+ bindableAttributes,
+ value
+ )
+ }
+ />
+
+ ) : (
+ <>
+ { bindableAttributes.map( ( attribute ) => (
+
+ !! boundFields[ attribute ]
+ }
+ label={ attribute }
+ onDeselect={ () =>
+ handleFieldChange( attribute, '' )
+ }
+ isShownByDefault={ true }
+ >
+
+ handleFieldChange(
+ attribute,
+ value
+ )
+ }
+ />
+
+ ) ) }
+ >
+ ) }
+
+
+ >
+ );
+ };
+}, 'withCustomControls' );
+
+if ( window.scf?.betaFeatures?.connect_fields ) {
+ addFilter(
+ 'editor.BlockEdit',
+ 'secure-custom-fields/with-custom-controls',
+ withCustomControls
+ );
+}
diff --git a/assets/src/js/bindings/components/block-attributes-control-linked-button.js b/assets/src/js/bindings/components/block-attributes-control-linked-button.js
new file mode 100644
index 00000000..5824344a
--- /dev/null
+++ b/assets/src/js/bindings/components/block-attributes-control-linked-button.js
@@ -0,0 +1,27 @@
+/**
+ * WordPress dependencies
+ */
+import { link, linkOff } from '@wordpress/icons';
+import { __ } from '@wordpress/i18n';
+import { Button } from '@wordpress/components';
+
+const BlockAttributesControlLinkedButton = (
+ props
+) => {
+ const { className, isLinked, onClick } = props;
+
+ const label = isLinked ? __( 'Unlink sides' ) : __( 'Link sides' );
+
+ return (
+
+ );
+};
+
+export default BlockAttributesControlLinkedButton;
\ No newline at end of file
diff --git a/assets/src/js/bindings/index.js b/assets/src/js/bindings/index.js
index 278d4879..c81a6544 100644
--- a/assets/src/js/bindings/index.js
+++ b/assets/src/js/bindings/index.js
@@ -1 +1,2 @@
import './sources.js';
+import './block-editor.js';
diff --git a/assets/src/js/bindings/sources.js b/assets/src/js/bindings/sources.js
index 0dcfced6..cbbbd1ae 100644
--- a/assets/src/js/bindings/sources.js
+++ b/assets/src/js/bindings/sources.js
@@ -4,6 +4,7 @@
import { __ } from '@wordpress/i18n';
import { registerBlockBindingsSource } from '@wordpress/blocks';
import { store as coreDataStore } from '@wordpress/core-data';
+import { dateI18n } from '@wordpress/date';
/**
* Get the value of a specific field from the ACF fields.
@@ -24,6 +25,8 @@ const resolveImageAttribute = ( imageObj, attribute ) => {
return imageObj.alt_text || '';
case 'title':
return imageObj.title?.rendered || '';
+ case 'id':
+ return imageObj.id;
default:
return '';
}
@@ -43,12 +46,19 @@ registerBlockBindingsSource( {
)
: undefined;
const result = {};
-
Object.entries( bindings ).forEach(
( [ attribute, { args } = {} ] ) => {
const fieldName = args?.key;
-
+ const mergedFields = fields?.scf_field_groups
+ ? Object.fromEntries(
+ fields.scf_field_groups
+ .flatMap( ( group ) => group.fields || [] )
+ .map( ( field ) => [ field.name, field ] )
+ )
+ : {};
const fieldValue = getFieldValue( fields, fieldName );
+ const fieldType = mergedFields[ fieldName ]?.type;
+
if ( typeof fieldValue === 'object' && fieldValue !== null ) {
let value = '';
@@ -59,7 +69,7 @@ registerBlockBindingsSource( {
}
result[ attribute ] = value;
- } else if ( typeof fieldValue === 'number' ) {
+ } else if ( 'number' === typeof fieldValue ) {
if ( attribute === 'content' ) {
result[ attribute ] = fieldValue.toString() || '';
} else {
@@ -69,6 +79,12 @@ registerBlockBindingsSource( {
attribute
);
}
+ } else if ( 'date_picker' === fieldType && fieldValue ) {
+ result[ attribute ] =
+ dateI18n(
+ mergedFields[ fieldName ]?.display_format,
+ fieldValue
+ ) || '';
} else {
result[ attribute ] = fieldValue || '';
}
diff --git a/assets/src/sass/_forms.scss b/assets/src/sass/_forms.scss
index 7cb7883d..c9430736 100644
--- a/assets/src/sass/_forms.scss
+++ b/assets/src/sass/_forms.scss
@@ -313,5 +313,4 @@ p.submit .acf-spinner {
margin: 0;
}
}
-}
-
+}
\ No newline at end of file
diff --git a/includes/admin/beta-features.php b/includes/admin/beta-features.php
index 1051fd58..9baebbe4 100644
--- a/includes/admin/beta-features.php
+++ b/includes/admin/beta-features.php
@@ -35,8 +35,7 @@ class SCF_Admin_Beta_Features {
* @return void
*/
public function __construct() {
- // Temporarily disabled - will be enabled when beta feature is ready
- // add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 );
+ add_action( 'admin_menu', array( $this, 'admin_menu' ), 20 );
}
/**
@@ -78,26 +77,6 @@ public function get_beta_features() {
return $this->beta_features;
}
- /**
- * Localizes the beta features data.
- *
- * @since SCF 6.5.0
- *
- * @return void
- */
- public function localize_beta_features() {
- $beta_features = array();
- foreach ( $this->get_beta_features() as $name => $beta_feature ) {
- $beta_features[ $name ] = $beta_feature->is_enabled();
- }
-
- acf_localize_data(
- array(
- 'betaFeatures' => $beta_features,
- )
- );
- }
-
/**
* This function will add the SCF beta features menu item to the WP admin
*
@@ -115,7 +94,7 @@ public function admin_menu() {
$page = add_submenu_page( 'edit.php?post_type=acf-field-group', __( 'Beta Features', 'secure-custom-fields' ), __( 'Beta Features', 'secure-custom-fields' ), acf_get_setting( 'capability' ), 'scf-beta-features', array( $this, 'html' ) );
add_action( 'load-' . $page, array( $this, 'load' ) );
- add_action( 'admin_enqueue_scripts', array( $this, 'localize_beta_features' ), 20 );
+ add_action( 'admin_enqueue_scripts', array( $this, 'add_beta_features_script' ), 20 );
}
/**
@@ -155,7 +134,7 @@ public function admin_body_class( $classes ) {
*/
private function include_beta_features() {
acf_include( 'includes/admin/beta-features/class-scf-beta-feature.php' );
- acf_include( 'includes/admin/beta-features/class-scf-beta-feature-editor-sidebar.php' );
+ acf_include( 'includes/admin/beta-features/class-scf-beta-feature-connect-fields.php' );
add_action( 'scf/include_admin_beta_features', array( $this, 'register_beta_features' ) );
@@ -170,7 +149,7 @@ private function include_beta_features() {
* @return void
*/
public function register_beta_features() {
- scf_register_admin_beta_feature( 'SCF_Admin_Beta_Feature_Editor_Sidebar' );
+ scf_register_admin_beta_feature( 'SCF_Admin_Beta_Feature_Connect_Fields' );
}
/**
@@ -255,6 +234,28 @@ public function metabox_html( $post, $metabox ) {
acf_nonce_input( $beta_feature->name );
echo '';
}
+
+ /**
+ * Adds the editor sidebar script to the page.
+ *
+ * @since SCF 6.5.0
+ *
+ * @return void
+ */
+ public function add_beta_features_script() {
+ // Check if the connected fields feature is enabled
+
+ $script = 'window.scf = window.scf || {};
+window.scf = window.scf || {};
+window.scf.betaFeatures = window.scf.betaFeatures || {};';
+ foreach ( $this->get_beta_features() as $name => $beta_feature ) {
+ if ( $beta_feature->is_enabled() ) {
+ $script .= sprintf( 'window.scf.betaFeatures.%s = true;', esc_js( $name ) );
+ }
+ }
+
+ wp_add_inline_script( 'wp-block-editor', $script, 'before' );
+ }
}
// initialize
diff --git a/includes/admin/beta-features/class-scf-beta-feature-connect-fields.php b/includes/admin/beta-features/class-scf-beta-feature-connect-fields.php
new file mode 100644
index 00000000..5f09643e
--- /dev/null
+++ b/includes/admin/beta-features/class-scf-beta-feature-connect-fields.php
@@ -0,0 +1,38 @@
+name = 'connect_fields';
+ $this->title = __( 'Connect Fields', 'secure-custom-fields' );
+ $this->description = __( 'Connects field to binding compatible blocks.', 'secure-custom-fields' );
+ }
+ }
+endif;
diff --git a/includes/rest-api/class-acf-rest-types-endpoint.php b/includes/rest-api/class-acf-rest-types-endpoint.php
index d1867cbd..7a75c86d 100644
--- a/includes/rest-api/class-acf-rest-types-endpoint.php
+++ b/includes/rest-api/class-acf-rest-types-endpoint.php
@@ -37,11 +37,12 @@ public function __construct() {
* @return void
*/
public function register_extra_fields() {
- if ( ! (bool) get_option( 'scf_beta_feature_editor_sidebar_enabled', false ) ) {
+ if ( ! (bool) get_option( 'scf_beta_feature_connect_fields_enabled', false ) ) {
return;
}
+ $post_types = get_post_types( array( 'show_in_rest' => true ) );
register_rest_field(
- 'type',
+ $post_types,
'scf_field_groups',
array(
'get_callback' => array( $this, 'get_scf_fields' ),
@@ -50,16 +51,19 @@ public function register_extra_fields() {
);
}
- /**
- * Get SCF fields for a post type.
- *
- * @since SCF 6.5.0
- *
- * @param array $post_type_object The post type object.
- * @return array Array of field data.
- */
+ /**
+ * Get SCF fields for a post type.
+ *
+ * @since 6.5.0
+ *
+ * @param array $post_type_object The post type object.
+ * @return array Array of field data.
+ */
public function get_scf_fields( $post_type_object ) {
- $post_type = $post_type_object['slug'];
+ if ( ! isset( $post_type_object['id'] ) || ! isset( $post_type_object['type'] ) ) {
+ return array();
+ }
+ $post_type = $post_type_object['type'];
$field_groups = acf_get_field_groups( array( 'post_type' => $post_type ) );
$field_groups_data = array();
@@ -69,8 +73,10 @@ public function get_scf_fields( $post_type_object ) {
foreach ( $fields as $field ) {
$group_fields[] = array(
- 'label' => $field['label'],
- 'type' => $field['type'],
+ 'label' => $field['label'],
+ 'type' => $field['type'],
+ 'name' => $field['name'],
+ 'display_format' => isset( $field['display_format'] ) ? $field['display_format'] : '',
);
}
diff --git a/package-lock.json b/package-lock.json
index 51abb472..6116c2ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,6 +5,7 @@
"packages": {
"": {
"dependencies": {
+ "@wordpress/icons": "^10.26.0",
"md5": "^2.3.0"
},
"devDependencies": {
diff --git a/package.json b/package.json
index d2a2a547..48c2a22f 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"watch": "webpack --watch"
},
"dependencies": {
+ "@wordpress/icons": "^10.26.0",
"md5": "^2.3.0"
},
"devDependencies": {
diff --git a/secure-custom-fields.php b/secure-custom-fields.php
index 6a98a943..2475a3b6 100644
--- a/secure-custom-fields.php
+++ b/secure-custom-fields.php
@@ -878,7 +878,7 @@ function scf_plugin_deactivated_notice() {
function scf_plugin_uninstall() {
// List of known beta features.
$beta_features = array(
- 'editor_sidebar',
+ 'connect_fields',
);
foreach ( $beta_features as $beta_feature ) {