Skip to content
Open
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
43 changes: 34 additions & 9 deletions assets/src/js/_acf-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -746,13 +746,20 @@
*
* @date 20/10/2021
* @since ACF 5.11.0
*
* @param {jQuery} [$form] - Optional form element to scope the input search to. If omitted, searches the entire document.
*/
var ensureInvalidFieldVisibility = function () {
// Load each ACF input field and check it's browser validation state.
var $inputs = $( '.acf-field input' );
const ensureInvalidFieldVisibility = function ( $form ) {
// Load each ACF input field and check its browser validation state.
// Scope to the given form if provided, otherwise search the entire document.
const $inputs = $form
? $form.find( '.acf-field input' )
: $( '.acf-field input' );
$inputs.each( function () {
if ( ! this.checkValidity() ) {
// Field is invalid, so we need to make sure it's metabox is visible.
// Use validity.valid (a property) instead of checkValidity() (a method) to avoid
// dispatching the native 'invalid' event, which can trigger ACF validation prematurely.
if ( ! this.validity.valid ) {
// Field is invalid, so we need to make sure its metabox is visible.
ensureFieldPostBoxIsVisible( $( this ) );
}
} );
Expand Down Expand Up @@ -924,19 +931,37 @@
*
* Callback when clicking submit.
*
* Only acts on submit buttons that are associated with a form containing ACF fields.
* This prevents submit buttons in unrelated UI (e.g. the WP link inserter modal) from
* triggering premature validation of ACF fields on the page behind the modal.
*
* Uses the native HTMLButtonElement.form property to resolve the associated form,
* which correctly handles both DOM-ancestor forms and the HTML form="id" attribute.
*
* @date 4/9/18
* @since ACF 5.7.5
*
* @param object e The event object.
* @param jQuery $el The input element.
* @param jQuery $el The submit button element.
* @return void
*/
onClickSubmit: function ( e, $el ) {
const form = $el.length ? $el[ 0 ].form : null;

if ( ! form ) {
return;
}

const $form = $( form );

if ( ! $form.find( '.acf-field' ).length ) {
return;
}

// Some browsers (safari) force their browser validation before our AJAX validation,
// so we need to make sure fields are visible earlier than showErrors()
ensureInvalidFieldVisibility();
// so we need to make sure fields are visible earlier than showErrors().
ensureInvalidFieldVisibility( $form );

// store the "click event" for later use in this.onSubmit()
this.set( 'originalEvent', e );
},

Expand Down
8 changes: 6 additions & 2 deletions assets/src/js/pro/_acf-blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -740,10 +740,14 @@ const md5 = require( 'md5' );
// Replace names for JSX counterparts.
name = getJSXName( name );

// Convert JSON values.
// Convert JSON values. Ignore values that merely start with a
// JSON-like character but aren't valid JSON (e.g. an oEmbed
// title beginning with "[" or "{").
const c1 = value.charAt( 0 );
if ( c1 === '[' || c1 === '{' ) {
value = JSON.parse( value );
try {
value = JSON.parse( value );
} catch ( err ) {}
}

// Convert bool values.
Expand Down
63 changes: 63 additions & 0 deletions assets/src/js/pro/blocks-v3/components/block-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,63 @@ const useBlockEditorInspectorSidebarOpen = () => {
return isOpen;
};

/**
* Converts serialized Google Map field values from JSON strings back into
* objects so they aren't double-encoded when the block's data is stored as
* a JSON block attribute. Google Map fields keep their value as a JSON
* string in a hidden input, which acf.serialize() would otherwise pass
* through as a string.
*
* @param {Object} $form The block form jQuery element that was serialized.
* @param {Object} serializedData The data returned by acf.serialize(); modified in place.
* @param {string} clientId The block client ID.
* @return {void}
*/
function deserializeGoogleMapValues( $form, serializedData, clientId ) {
const inputPrefix = `acf-block_${ clientId }`;

$form
.find( '.acf-field-google-map input[type="hidden"][name]' )
.each( function () {
if ( this.name.indexOf( inputPrefix ) !== 0 ) {
return;
}

// Extract the bracketed key path, e.g. "[field_abc][field_def]" -> [ 'field_abc', 'field_def' ].
const keyPath = this.name
.slice( inputPrefix.length )
.match( /([^\[\]])+/g );
if ( ! keyPath ) {
return;
}

// Walk the serialized data down to the parent of the leaf key.
let parent = serializedData;
for ( let i = 0; i < keyPath.length - 1; i++ ) {
if ( parent === null || typeof parent !== 'object' ) {
return;
}
parent = parent[ keyPath[ i ] ];
}
if ( parent === null || typeof parent !== 'object' ) {
return;
}

const leafKey = keyPath[ keyPath.length - 1 ];
if ( typeof parent[ leafKey ] === 'string' ) {
try {
const parsedValue = JSON.parse( parent[ leafKey ] );
if (
typeof parsedValue === 'object' &&
parsedValue !== null
) {
parent[ leafKey ] = parsedValue;
}
} catch ( err ) {}
}
} );
}

/**
* Main BlockEdit component wrapper
* Manages block data fetching and initial setup
Expand Down Expand Up @@ -925,6 +982,7 @@ function BlockEditInner( props ) {
`acf-block_${ clientId }`
);
if ( serializedData ) {
deserializeGoogleMapValues( $form, serializedData, clientId );
setTheSerializedAcfData( JSON.stringify( serializedData ) );
} else {
setUserHasInteractedWithForm( false );
Expand Down Expand Up @@ -1167,6 +1225,11 @@ function BlockEditInner( props ) {
`acf-block_${ clientId }`
);
if ( serializedData ) {
deserializeGoogleMapValues(
$form,
serializedData,
clientId
);
setTheSerializedAcfData(
JSON.stringify( serializedData )
);
Expand Down
6 changes: 5 additions & 1 deletion assets/src/js/pro/blocks-v3/components/jsx-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,13 @@ function parseAttribute( attribute ) {
}

attrName = getJSXNameReplacement( attrName );
// Ignore values that merely start with a JSON-like character but
// aren't valid JSON (e.g. an oEmbed title beginning with "[" or "{").
const firstChar = attrValue.charAt( 0 );
if ( firstChar === '[' || firstChar === '{' ) {
attrValue = JSON.parse( attrValue );
try {
attrValue = JSON.parse( attrValue );
} catch ( err ) {}
}
if ( attrValue === 'true' || attrValue === 'false' ) {
attrValue = attrValue === 'true';
Expand Down
50 changes: 50 additions & 0 deletions assets/src/sass/acf-input.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@charset "UTF-8";
@use "mixins" as *;
/*--------------------------------------------------------------------------------------------
*
* Vars
Expand Down Expand Up @@ -586,6 +587,14 @@ body.acf-browser-firefox .acf-field select {
border-color: #dddddd;
min-height: 28px;
}
.acf-input-prepend,
.acf-input-append {
// WP 7.0+
@include wp-admin("7-0") {
min-height: 40px;
padding: 9px 8px;
}
}

.acf-input-prepend {
float: left;
Expand Down Expand Up @@ -738,6 +747,12 @@ html[dir=rtl] input.acf-is-prepended.acf-is-appended {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07) inset;
min-height: 31px;
}
.select2-container.-acf .select2-choices {
// WP 7.0+
@include wp-admin("7-0") {
min-height: 40px;
}
}
.select2-container.-acf .select2-choices .select2-search-choice {
margin: 5px 0 5px 5px;
padding: 3px 5px 3px 18px;
Expand Down Expand Up @@ -840,6 +855,41 @@ html[dir=rtl] .select2-container.-acf .select2-choice .select2-arrow {
.acf-admin-3-8 .select2-container.-acf .select2-selection {
border-color: #aaa;
}
.select2-container.-acf {
// WP 7.0+
@include wp-admin("7-0") {
.select2-selection--single {
height: 40px;

.select2-selection__rendered {
line-height: 38px;
}
}

.select2-selection--multiple {
min-height: 40px;
line-height: 0;

.select2-selection__rendered {
line-height: normal;
display: flex;
align-items: center;
flex-wrap: wrap;
min-height: 38px;
}

.select2-search__field {
margin-top: 0;
}

.select2-selection__choice {
margin-top: 0;
padding-top: 4px;
padding-bottom: 4px;
}
}
}
}
.select2-container.-acf .select2-selection--multiple .select2-search--inline:first-child {
float: none;
}
Expand Down
4 changes: 4 additions & 0 deletions includes/admin/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ public function admin_body_class( $classes ) {
// Add body class.
$classes .= ' acf-admin-5-3';

if ( version_compare( $wp_version, '7.0', '>=' ) ) {
$classes .= ' acf-admin-7-0';
}

// Add browser for specific CSS.
$classes .= ' acf-browser-' . esc_attr( acf_get_browser() );

Expand Down
54 changes: 43 additions & 11 deletions includes/blocks-auto-inline-editing.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,20 @@ function get_non_auto_inline_editing_fields(): array {
*/
function populate_auto_inline_editing_values( $field_value, $post_id, $field ) {

global $acf_fields_used_in_block_render_template, $acf_blocks_doing_auto_inline_editing;
global $acf_fields_used_in_block_render_template, $acf_blocks_doing_auto_inline_editing, $acf_blocks_doing_auto_inline_editing_block_id;

if ( ! $acf_blocks_doing_auto_inline_editing || ! empty( $field['parent_repeater'] ) ) {
return $field_value;
}

// Only modify values for fields fetched against the current block's own post id.
// Calls like get_field( 'foo', $other_post_id ) target a different post and must be returned unmodified
// so block templates can rely on real values (e.g. for conditional logic) instead of placeholder strings.
if ( ! empty( $acf_blocks_doing_auto_inline_editing_block_id )
&& (string) $post_id !== (string) $acf_blocks_doing_auto_inline_editing_block_id ) {
return $field_value;
}

// Add this field and its value to the global variable so we can grab it when rendering later in apply_inline_editing_attributes_to_render_template.
if ( ! is_array( $field_value ) ) {
if ( empty( $field_value ) ) {
Expand Down Expand Up @@ -90,21 +98,33 @@ function populate_auto_inline_editing_values( $field_value, $post_id, $field ) {
* @return string
*/
function apply_inline_editing_attributes_to_render_template( $path, $block, $content, $is_preview, $post_id, $wp_block, $context ): string {
global $acf_fields_used_in_block_render_template, $acf_blocks_doing_auto_inline_editing;
global $acf_fields_used_in_block_render_template, $acf_blocks_doing_auto_inline_editing, $acf_blocks_doing_auto_inline_editing_block_id;

unset( $content, $is_preview, $post_id, $wp_block, $context );

$acf_fields_used_in_block_render_template = array();
// Save the entire previous render context (fields collected, doing-flag, block id) so a
// nested SCF block render does not clobber the parent's tracked fields or active block id.
$previous_fields = is_array( $acf_fields_used_in_block_render_template ) ? $acf_fields_used_in_block_render_template : array();
$previous_doing_auto_inline_editing = ! empty( $acf_blocks_doing_auto_inline_editing );
$previous_block_id = $acf_blocks_doing_auto_inline_editing_block_id ?? null;

$acf_blocks_doing_auto_inline_editing = true;
$acf_fields_used_in_block_render_template = array();
$acf_blocks_doing_auto_inline_editing = true;
$acf_blocks_doing_auto_inline_editing_block_id = $block['id'] ?? null;

ob_start();
include $path;
$html = ob_get_clean();

$acf_blocks_doing_auto_inline_editing = false;
// Process the HTML before restoring the parent context - apply_inline_editing_attributes_to_html_string()
// reads $acf_fields_used_in_block_render_template via global, so it must see THIS block's field list.
$processed_html = apply_inline_editing_attributes_to_html_string( $html, $block );

return apply_inline_editing_attributes_to_html_string( $html, $block );
$acf_fields_used_in_block_render_template = $previous_fields;
$acf_blocks_doing_auto_inline_editing = $previous_doing_auto_inline_editing;
$acf_blocks_doing_auto_inline_editing_block_id = $previous_block_id;

return $processed_html;
}

/**
Expand All @@ -120,19 +140,31 @@ function apply_inline_editing_attributes_to_render_template( $path, $block, $con
* @return string
*/
function apply_inline_editing_attributes_to_render_callback( $render_callback, $block, $content, $is_preview, $post_id, $wp_block, $context ): string {
global $acf_fields_used_in_block_render_template, $acf_blocks_doing_auto_inline_editing;
global $acf_fields_used_in_block_render_template, $acf_blocks_doing_auto_inline_editing, $acf_blocks_doing_auto_inline_editing_block_id;

$acf_fields_used_in_block_render_template = array();
// Save the entire previous render context (fields collected, doing-flag, block id) so a
// nested SCF block render does not clobber the parent's tracked fields or active block id.
$previous_fields = is_array( $acf_fields_used_in_block_render_template ) ? $acf_fields_used_in_block_render_template : array();
$previous_doing_auto_inline_editing = ! empty( $acf_blocks_doing_auto_inline_editing );
$previous_block_id = $acf_blocks_doing_auto_inline_editing_block_id ?? null;

$acf_blocks_doing_auto_inline_editing = true;
$acf_fields_used_in_block_render_template = array();
$acf_blocks_doing_auto_inline_editing = true;
$acf_blocks_doing_auto_inline_editing_block_id = $block['id'] ?? null;

ob_start();
call_user_func( $render_callback, $block, $content, $is_preview, $post_id, $wp_block, $context );
$html = ob_get_clean();

$acf_blocks_doing_auto_inline_editing = false;
// Process the HTML before restoring the parent context - apply_inline_editing_attributes_to_html_string()
// reads $acf_fields_used_in_block_render_template via global, so it must see THIS block's field list.
$processed_html = apply_inline_editing_attributes_to_html_string( $html, $block );

$acf_fields_used_in_block_render_template = $previous_fields;
$acf_blocks_doing_auto_inline_editing = $previous_doing_auto_inline_editing;
$acf_blocks_doing_auto_inline_editing_block_id = $previous_block_id;

return apply_inline_editing_attributes_to_html_string( $html, $block );
return $processed_html;
}

/**
Expand Down
Loading
Loading