PluginProbe ʕ •ᴥ•ʔ
Secure Custom Fields / 6.7.0
Secure Custom Fields v6.7.0
6.9.5 6.9.4 6.9.3 6.9.2 6.9.1 6.9.0 6.8.9 6.8.7 6.8.8 6.8.6 6.8.4 6.8.5 trunk 6.4.0-beta1 6.4.0-beta2 6.4.1 6.4.1-beta3 6.4.1-beta4 6.4.1-beta5 6.4.1-beta6 6.4.1-beta7 6.4.2 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.6.0 6.7.0 6.7.1 6.8.0 6.8.1 6.8.2 6.8.3
secure-custom-fields / assets / src / js / bindings / block-editor.js
secure-custom-fields / assets / src / js / bindings Last commit date
block-editor.js 9 months ago index.js 1 year ago sources.js 1 year ago
block-editor.js
315 lines
1 /**
2 * WordPress dependencies
3 */
4 import { useState, useEffect, useCallback, useMemo } from '@wordpress/element';
5 import { addFilter } from '@wordpress/hooks';
6 import { createHigherOrderComponent } from '@wordpress/compose';
7 import {
8 InspectorControls,
9 useBlockBindingsUtils,
10 } from '@wordpress/block-editor';
11 import {
12 ComboboxControl,
13 __experimentalToolsPanel as ToolsPanel,
14 __experimentalToolsPanelItem as ToolsPanelItem,
15 } from '@wordpress/components';
16 import { __ } from '@wordpress/i18n';
17 import { useSelect } from '@wordpress/data';
18 import { store as coreDataStore } from '@wordpress/core-data';
19 import { store as editorStore } from '@wordpress/editor';
20
21 // These constant and the function above have been copied from Gutenberg. It should be public, eventually.
22
23 const BLOCK_BINDINGS_CONFIG = {
24 'core/paragraph': {
25 content: [ 'text', 'textarea', 'date_picker', 'number', 'range' ],
26 },
27 'core/heading': {
28 content: [ 'text', 'textarea', 'date_picker', 'number', 'range' ],
29 },
30 'core/image': {
31 id: [ 'image' ],
32 url: [ 'image' ],
33 title: [ 'image' ],
34 alt: [ 'image' ],
35 },
36 'core/button': {
37 url: [ 'url' ],
38 text: [ 'text', 'checkbox', 'select', 'date_picker' ],
39 linkTarget: [ 'text', 'checkbox', 'select' ],
40 rel: [ 'text', 'checkbox', 'select' ],
41 },
42 };
43
44 /**
45 * Gets the bindable attributes for a given block.
46 *
47 * @param {string} blockName The name of the block.
48 *
49 * @return {string[]} The bindable attributes for the block.
50 */
51 function getBindableAttributes( blockName ) {
52 const config = BLOCK_BINDINGS_CONFIG[ blockName ];
53 return config ? Object.keys( config ) : [];
54 }
55
56 /**
57 * Add custom controls to all blocks
58 */
59 const withCustomControls = createHigherOrderComponent( ( BlockEdit ) => {
60 return ( props ) => {
61 const bindableAttributes = getBindableAttributes( props.name );
62 const { updateBlockBindings, removeAllBlockBindings } =
63 useBlockBindingsUtils();
64
65 // Get ACF fields for current post
66 const fields = useSelect( ( select ) => {
67 const { getEditedEntityRecord } = select( coreDataStore );
68 const { getCurrentPostType, getCurrentPostId } =
69 select( editorStore );
70
71 const postType = getCurrentPostType();
72 const postId = getCurrentPostId();
73
74 if ( ! postType || ! postId ) return {};
75
76 const record = getEditedEntityRecord(
77 'postType',
78 postType,
79 postId
80 );
81
82 // Extract fields that end with '_source' (simplified)
83 const sourcedFields = {};
84 Object.entries( record?.acf || {} ).forEach( ( [ key, value ] ) => {
85 if ( key.endsWith( '_source' ) ) {
86 const baseFieldName = key.replace( '_source', '' );
87 if ( record?.acf.hasOwnProperty( baseFieldName ) ) {
88 sourcedFields[ baseFieldName ] = value;
89 }
90 }
91 } );
92 return sourcedFields;
93 }, [] );
94
95 // Get filtered field options for an attribute
96 const getFieldOptions = useCallback(
97 ( attribute = null ) => {
98 if ( ! fields || Object.keys( fields ).length === 0 ) return [];
99
100 const blockConfig = BLOCK_BINDINGS_CONFIG[ props.name ];
101 let allowedTypes = null;
102
103 if ( blockConfig ) {
104 allowedTypes = attribute
105 ? blockConfig[ attribute ]
106 : Object.values( blockConfig ).flat();
107 }
108
109 return Object.entries( fields )
110 .filter(
111 ( [ , fieldConfig ] ) =>
112 ! allowedTypes ||
113 allowedTypes.includes( fieldConfig.type )
114 )
115 .map( ( [ fieldName, fieldConfig ] ) => ( {
116 value: fieldName,
117 label: fieldConfig.label,
118 } ) );
119 },
120 [ fields, props.name ]
121 );
122
123 // Check if all attributes use the same field types (for "all attributes" mode)
124 const canUseAllAttributesMode = useMemo( () => {
125 if ( ! bindableAttributes || bindableAttributes.length <= 1 )
126 return false;
127
128 const blockConfig = BLOCK_BINDINGS_CONFIG[ props.name ];
129 if ( ! blockConfig ) return false;
130
131 const firstAttributeTypes =
132 blockConfig[ bindableAttributes[ 0 ] ] || [];
133 return bindableAttributes.every( ( attr ) => {
134 const attrTypes = blockConfig[ attr ] || [];
135 return (
136 attrTypes.length === firstAttributeTypes.length &&
137 attrTypes.every( ( type ) =>
138 firstAttributeTypes.includes( type )
139 )
140 );
141 } );
142 }, [ bindableAttributes, props.name ] );
143
144 // Track bound fields
145 const [ boundFields, setBoundFields ] = useState( {} );
146
147 // Sync with current bindings
148 useEffect( () => {
149 const currentBindings = props.attributes?.metadata?.bindings || {};
150 const newBoundFields = {};
151
152 Object.keys( currentBindings ).forEach( ( attribute ) => {
153 if ( currentBindings[ attribute ]?.args?.key ) {
154 newBoundFields[ attribute ] =
155 currentBindings[ attribute ].args.key;
156 }
157 } );
158
159 setBoundFields( newBoundFields );
160 }, [ props.attributes?.metadata?.bindings ] );
161
162 // Handle field selection
163 const handleFieldChange = useCallback(
164 ( attribute, value ) => {
165 if ( Array.isArray( attribute ) ) {
166 // Handle multiple attributes at once
167 const newBoundFields = { ...boundFields };
168 const bindings = {};
169
170 attribute.forEach( ( attr ) => {
171 newBoundFields[ attr ] = value;
172 bindings[ attr ] = value
173 ? {
174 source: 'acf/field',
175 args: { key: value },
176 }
177 : undefined;
178 } );
179
180 setBoundFields( newBoundFields );
181 updateBlockBindings( bindings );
182 } else {
183 // Handle single attribute
184 setBoundFields( ( prev ) => ( {
185 ...prev,
186 [ attribute ]: value,
187 } ) );
188 updateBlockBindings( {
189 [ attribute ]: value
190 ? {
191 source: 'acf/field',
192 args: { key: value },
193 }
194 : undefined,
195 } );
196 }
197 },
198 [ boundFields, updateBlockBindings ]
199 );
200
201 // Handle reset
202 const handleReset = useCallback( () => {
203 removeAllBlockBindings();
204 setBoundFields( {} );
205 }, [ removeAllBlockBindings ] );
206
207 // Don't show if no fields or attributes
208 const fieldOptions = getFieldOptions();
209 if ( fieldOptions.length === 0 || ! bindableAttributes ) {
210 return <BlockEdit { ...props } />;
211 }
212
213 if ( bindableAttributes.length === 0 ) {
214 return <BlockEdit { ...props } />;
215 }
216
217 return (
218 <>
219 <InspectorControls { ...props }>
220 <ToolsPanel
221 label={ __(
222 'Connect to a field',
223 'secure-custom-fields'
224 ) }
225 resetAll={ handleReset }
226 >
227 { canUseAllAttributesMode ? (
228 <ToolsPanelItem
229 hasValue={ () =>
230 !! boundFields[ bindableAttributes[ 0 ] ]
231 }
232 label={ __(
233 'All attributes',
234 'secure-custom-fields'
235 ) }
236 onDeselect={ () =>
237 handleFieldChange(
238 bindableAttributes,
239 null
240 )
241 }
242 isShownByDefault={ true }
243 >
244 <ComboboxControl
245 label={ __(
246 'Field',
247 'secure-custom-fields'
248 ) }
249 placeholder={ __(
250 'Select a field',
251 'secure-custom-fields'
252 ) }
253 options={ getFieldOptions() }
254 value={
255 boundFields[
256 bindableAttributes[ 0 ]
257 ] || ''
258 }
259 onChange={ ( value ) =>
260 handleFieldChange(
261 bindableAttributes,
262 value
263 )
264 }
265 __next40pxDefaultSize
266 __nextHasNoMarginBottom
267 />
268 </ToolsPanelItem>
269 ) : (
270 bindableAttributes.map( ( attribute ) => (
271 <ToolsPanelItem
272 key={ `scf-field-${ attribute }` }
273 hasValue={ () =>
274 !! boundFields[ attribute ]
275 }
276 label={ attribute }
277 onDeselect={ () =>
278 handleFieldChange( attribute, null )
279 }
280 isShownByDefault={ true }
281 >
282 <ComboboxControl
283 label={ attribute }
284 placeholder={ __(
285 'Select a field',
286 'secure-custom-fields'
287 ) }
288 options={ getFieldOptions( attribute ) }
289 value={ boundFields[ attribute ] || '' }
290 onChange={ ( value ) =>
291 handleFieldChange(
292 attribute,
293 value
294 )
295 }
296 __next40pxDefaultSize
297 __nextHasNoMarginBottom
298 />
299 </ToolsPanelItem>
300 ) )
301 ) }
302 </ToolsPanel>
303 </InspectorControls>
304 <BlockEdit { ...props } />
305 </>
306 );
307 };
308 }, 'withCustomControls' );
309
310 addFilter(
311 'editor.BlockEdit',
312 'secure-custom-fields/with-custom-controls',
313 withCustomControls
314 );
315