From 03d7128a01243bf17cb602e767e7b3bad916b73a Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:11:54 +0100 Subject: [PATCH 01/22] Backport all PHP --- includes/acf-field-functions.php | 7 +- includes/acf-field-group-functions.php | 28 ++++ includes/acf-input-functions.php | 22 ++- includes/acf-internal-post-type-functions.php | 2 +- .../admin/class-acf-admin-options-page.php | 11 +- .../views/acf-field-group/list-empty.php | 3 +- .../views/acf-field-group/location-rule.php | 1 - .../admin/views/acf-field-group/locations.php | 1 - .../admin/views/acf-field-group/options.php | 23 ++- .../admin/views/acf-post-type/list-empty.php | 3 +- includes/admin/views/upgrade/network.php | 14 +- includes/admin/views/upgrade/notice.php | 4 +- includes/ajax/class-acf-ajax-check-screen.php | 2 +- includes/blocks.php | 74 ++++++++-- includes/class-acf-internal-post-type.php | 2 +- .../fields/class-acf-field-button-group.php | 32 ++-- includes/fields/class-acf-field-checkbox.php | 6 + .../fields/class-acf-field-color_picker.php | 139 +++++++++++++++++- includes/fields/class-acf-field-file.php | 10 +- .../fields/class-acf-field-icon_picker.php | 7 +- includes/fields/class-acf-field-image.php | 10 +- includes/fields/class-acf-field-radio.php | 8 +- includes/fields/class-acf-field-repeater.php | 63 ++++++-- includes/fields/class-acf-field-taxonomy.php | 13 +- includes/forms/WC_Order.php | 3 +- includes/forms/form-comment.php | 2 +- includes/forms/form-nav-menu.php | 2 +- includes/forms/form-post.php | 13 +- includes/forms/form-taxonomy.php | 12 +- includes/forms/form-user.php | 4 +- includes/post-types/class-acf-field-group.php | 1 + includes/third-party.php | 4 +- 32 files changed, 435 insertions(+), 91 deletions(-) diff --git a/includes/acf-field-functions.php b/includes/acf-field-functions.php index a6d25e52..a4aa5f46 100644 --- a/includes/acf-field-functions.php +++ b/includes/acf-field-functions.php @@ -829,7 +829,12 @@ function acf_render_field_label( $field ) { // Output label. if ( $label ) { - echo '' . acf_esc_html( $label ) . ''; + // For multi-choice fields (radio, checkbox, taxonomy, button_group), don't use 'for' attribute but add ID for aria-labelledby + if ( in_array( $field['type'], array( 'radio', 'checkbox', 'taxonomy', 'button_group' ), true ) && $field['id'] ) { + echo ''; + } else { + echo '' . acf_esc_html( $label ) . ''; + } } } diff --git a/includes/acf-field-group-functions.php b/includes/acf-field-group-functions.php index d63013ac..c34d3a9c 100644 --- a/includes/acf-field-group-functions.php +++ b/includes/acf-field-group-functions.php @@ -540,3 +540,31 @@ function acf_field_group_has_location_type( int $post_id, string $location ) { return false; } + + +/** + * Retrieves the field group title, or display title if set. + * + * @since ACF 6.6 + * + * @param array|integer $field_group The field group array or ID. + * @return string The field group title. + */ +function acf_get_field_group_title( $field_group ): string { + if ( is_numeric( $field_group ) ) { + $field_group = acf_get_field_group( $field_group ); + } + + $title = ''; + if ( ! empty( $field_group['title'] ) && is_string( $field_group['title'] ) ) { + $title = $field_group['title']; + } + + // Override with the Display Title if set. + if ( ! empty( $field_group['display_title'] ) && is_string( $field_group['display_title'] ) ) { + $title = $field_group['display_title']; + } + + // Filter and return. + return apply_filters( 'acf/get_field_group_title', esc_html( $title ), $field_group ); +} diff --git a/includes/acf-input-functions.php b/includes/acf-input-functions.php index 4680a889..3e679a7b 100644 --- a/includes/acf-input-functions.php +++ b/includes/acf-input-functions.php @@ -333,7 +333,27 @@ function acf_get_checkbox_input( $attrs = array() ) { // Render. $checked = isset( $attrs['checked'] ); - return ' ' . acf_esc_html( $label ) . ''; + + // Build label attributes array for accessibility and consistency. + $label_attrs = array(); + if ( $checked ) { + $label_attrs['class'] = 'selected'; + } + + if ( ! empty( $attrs['button_group'] ) ) { + unset( $attrs['button_group'] ); + // If tabindex is provided, use it for the label; otherwise, use checked-based default. + if ( isset( $attrs['tabindex'] ) ) { + $label_attrs['tabindex'] = (string) $attrs['tabindex']; + unset( $attrs['tabindex'] ); + } else { + $label_attrs['tabindex'] = $checked ? '0' : '-1'; + } + $label_attrs['role'] = 'radio'; + $label_attrs['aria-checked'] = $checked ? 'true' : 'false'; + } + + return ' ' . acf_esc_html( $label ) . ''; } /** diff --git a/includes/acf-internal-post-type-functions.php b/includes/acf-internal-post-type-functions.php index affeb08d..e06aae9a 100644 --- a/includes/acf-internal-post-type-functions.php +++ b/includes/acf-internal-post-type-functions.php @@ -10,7 +10,7 @@ * Gets an instance of an ACF_Internal_Post_Type. * * @param string $post_type The ACF internal post type to get the instance for. - * @return ACF_Internal_Post_Type|bool The internal post type class instance, or false on failure. + * @return ACF_Internal_Post_Type|boolean The internal post type class instance, or false on failure. */ function acf_get_internal_post_type_instance( $post_type = 'acf-field-group' ) { $store = acf_get_store( 'internal-post-types' ); diff --git a/includes/admin/class-acf-admin-options-page.php b/includes/admin/class-acf-admin-options-page.php index f6cc0061..9a344e38 100644 --- a/includes/admin/class-acf-admin-options-page.php +++ b/includes/admin/class-acf-admin-options-page.php @@ -179,7 +179,6 @@ public function admin_head() { // vars $id = "acf-{$field_group['key']}"; - $title = $field_group['title']; $context = $field_group['position']; $priority = 'high'; $args = array( 'field_group' => $field_group ); @@ -195,7 +194,15 @@ public function admin_head() { $priority = apply_filters( 'acf/input/meta_box_priority', $priority, $field_group ); // add meta box - add_meta_box( $id, esc_html( $title ), array( $this, 'postbox_acf' ), 'acf_options_page', $context, $priority, $args ); + add_meta_box( + $id, + acf_esc_html( acf_get_field_group_title( $field_group ) ), + array( $this, 'postbox_acf' ), + 'acf_options_page', + $context, + $priority, + $args + ); } // foreach } diff --git a/includes/admin/views/acf-field-group/list-empty.php b/includes/admin/views/acf-field-group/list-empty.php index 02f154ba..2c6872f8 100644 --- a/includes/admin/views/acf-field-group/list-empty.php +++ b/includes/admin/views/acf-field-group/list-empty.php @@ -5,7 +5,8 @@ * @package wordpress/secure-custom-fields */ -?> +?> +
diff --git a/includes/admin/views/acf-field-group/location-rule.php b/includes/admin/views/acf-field-group/location-rule.php index a0caae22..44413909 100644 --- a/includes/admin/views/acf-field-group/location-rule.php +++ b/includes/admin/views/acf-field-group/location-rule.php @@ -2,7 +2,6 @@ // vars $prefix = 'acf_field_group[location][' . $rule['group'] . '][' . $rule['id'] . ']'; - ?> diff --git a/includes/admin/views/acf-field-group/locations.php b/includes/admin/views/acf-field-group/locations.php index fe00fb95..25df9992 100644 --- a/includes/admin/views/acf-field-group/locations.php +++ b/includes/admin/views/acf-field-group/locations.php @@ -2,7 +2,6 @@ // global global $field_group; - ?>
diff --git a/includes/admin/views/acf-field-group/options.php b/includes/admin/views/acf-field-group/options.php index 77c61360..a816c7ab 100644 --- a/includes/admin/views/acf-field-group/options.php +++ b/includes/admin/views/acf-field-group/options.php @@ -72,6 +72,8 @@ case 'location_rules': echo '
'; acf_get_view( 'acf-field-group/locations' ); + + do_action( 'acf/field_group/render_additional_location_settings', $field_group ); echo '
'; break; case 'presentation': @@ -164,6 +166,8 @@ 'field' ); + do_action( 'acf/field_group/render_additional_presentation_settings', $field_group ); + echo '
'; echo '
'; @@ -221,8 +225,6 @@ 'prefix' => 'acf_field_group', 'value' => $field_group['active'], 'ui' => 1, - // 'ui_on_text' => __('Active', 'secure-custom-fields'), - // 'ui_off_text' => __('Inactive', 'secure-custom-fields'), ) ); @@ -237,8 +239,6 @@ 'prefix' => 'acf_field_group', 'value' => $field_group['show_in_rest'], 'ui' => 1, - // 'ui_on_text' => __('Active', 'secure-custom-fields'), - // 'ui_off_text' => __('Inactive', 'secure-custom-fields'), ) ); } @@ -257,6 +257,21 @@ 'field' ); + acf_render_field_wrap( + array( + 'label' => __( 'Display Title', 'secure-custom-fields' ), + 'instructions' => __( 'Title shown on the edit screen for the field group meta box to use instead of the field group title', 'secure-custom-fields' ), + 'type' => 'text', + 'name' => 'display_title', + 'prefix' => 'acf_field_group', + 'value' => $field_group['display_title'], + ), + 'div', + 'field' + ); + + do_action( 'acf/field_group/render_additional_group_settings', $field_group ); + /* translators: 1: Post creation date 2: Post creation time */ $acf_created_on = sprintf( __( 'Created on %1$s at %2$s', 'secure-custom-fields' ), get_the_date(), get_the_time() ); ?> diff --git a/includes/admin/views/acf-post-type/list-empty.php b/includes/admin/views/acf-post-type/list-empty.php index f12219fd..0bfc2cca 100644 --- a/includes/admin/views/acf-post-type/list-empty.php +++ b/includes/admin/views/acf-post-type/list-empty.php @@ -5,7 +5,8 @@ * @package wordpress/secure-custom-fields */ -?> +?> +
diff --git a/includes/admin/views/upgrade/network.php b/includes/admin/views/upgrade/network.php index 49ab0457..643d4b1f 100644 --- a/includes/admin/views/upgrade/network.php +++ b/includes/admin/views/upgrade/network.php @@ -60,7 +60,7 @@ ?> class="alternate"> @@ -73,8 +73,16 @@ class="alternate"> - - + + + diff --git a/includes/admin/views/upgrade/notice.php b/includes/admin/views/upgrade/notice.php index bb14df96..603d8f2e 100644 --- a/includes/admin/views/upgrade/notice.php +++ b/includes/admin/views/upgrade/notice.php @@ -19,7 +19,7 @@ } ?> -
+
@@ -33,7 +33,7 @@
- +
diff --git a/includes/ajax/class-acf-ajax-check-screen.php b/includes/ajax/class-acf-ajax-check-screen.php index 6ee8cba8..0fe6ef7d 100644 --- a/includes/ajax/class-acf-ajax-check-screen.php +++ b/includes/ajax/class-acf-ajax-check-screen.php @@ -53,7 +53,7 @@ public function get_response( $request ) { $item = array( 'id' => esc_attr( 'acf-' . $field_group['key'] ), 'key' => esc_attr( $field_group['key'] ), - 'title' => esc_html( $field_group['title'] ), + 'title' => acf_esc_html( acf_get_field_group_title( $field_group ) ), 'position' => esc_attr( $field_group['position'] ), 'classes' => postbox_classes( 'acf-' . $field_group['key'], $args['screen'] ), 'style' => esc_attr( $field_group['style'] ), diff --git a/includes/blocks.php b/includes/blocks.php index c8765dd0..4d792ba3 100644 --- a/includes/blocks.php +++ b/includes/blocks.php @@ -51,6 +51,17 @@ function acf_handle_json_block_registration( $settings, $metadata ) { return $settings; } + /** + * Filters the default ACF block version for blocks registered via block.json. + * + * @since 6.6.0 + * + * @param integer $default_acf_block_version The default ACF block version. + * @param array $settings An array of block settings. + * @return integer + */ + $default_acf_block_version = apply_filters( 'acf/blocks/default_block_version', 2, $settings ); + // Setup SCF defaults. $settings = wp_parse_args( $settings, @@ -64,8 +75,7 @@ function acf_handle_json_block_registration( $settings, $metadata ) { 'uses_context' => array(), 'supports' => array(), 'attributes' => array(), - 'acf_block_version' => 2, - 'api_version' => 2, + 'acf_block_version' => $default_acf_block_version, 'validate' => true, 'validate_on_load' => true, 'use_post_meta' => false, @@ -127,6 +137,17 @@ function acf_handle_json_block_registration( $settings, $metadata ) { } } + if ( isset( $metadata['apiVersion'] ) ) { + // Use the apiVersion defined in block.json if it exists. + $settings['api_version'] = $metadata['apiVersion']; + } elseif ( $settings['acf_block_version'] >= 3 && version_compare( get_bloginfo( 'version' ), '6.3', '>=' ) ) { + // Otherwise, if we're on WP 6.3+ and the block is ACF block version 3 or greater, use apiVersion 3. + $settings['api_version'] = 3; + } else { + // Otherwise, default to apiVersion 2. + $settings['api_version'] = 2; + } + // Add the block name and registration path to settings. $settings['name'] = $metadata['name']; $settings['path'] = dirname( $metadata['file'] ); @@ -198,11 +219,28 @@ function acf_register_block_type( $block ) { // Set ACF required attributes. $block['attributes'] = acf_get_block_type_default_attributes( $block ); - if ( ! isset( $block['api_version'] ) ) { - $block['api_version'] = 2; - } + + /** + * Filters the default ACF block version for blocks registered via acf_register_block_type(). + * + * @since 6.6.0 + * + * @param integer $default_acf_block_version The default ACF block version. + * @param array $block An array of block settings. + * @return integer + */ + $default_acf_block_version = apply_filters( 'acf/blocks/default_block_version', 1, $block ); + if ( ! isset( $block['acf_block_version'] ) ) { - $block['acf_block_version'] = 1; + $block['acf_block_version'] = $default_acf_block_version; + } + + if ( ! isset( $block['api_version'] ) ) { + if ( $block['acf_block_version'] >= 3 && version_compare( get_bloginfo( 'version' ), '6.3', '>=' ) ) { + $block['api_version'] = 3; + } else { + $block['api_version'] = 2; + } } // Add to storage. @@ -559,8 +597,13 @@ function acf_render_block_callback( $attributes, $content = '', $wp_block = null * @return string The block HTML. */ function acf_rendered_block( $attributes, $content = '', $is_preview = false, $post_id = 0, $wp_block = null, $context = false, $is_ajax_render = false ) { - $mode = isset( $attributes['mode'] ) ? $attributes['mode'] : 'auto'; - $form = ( 'edit' === $mode && $is_preview ); + if ( isset( $wp_block->block_type->acf_block_version ) && $wp_block->block_type->acf_block_version >= 3 ) { + $mode = 'preview'; + $form = false; + } else { + $mode = isset( $attributes['mode'] ) ? $attributes['mode'] : 'auto'; + $form = ( 'edit' === $mode && $is_preview ); + } // If context is available from the WP_Block class object and we have no context of our own, use that. if ( empty( $context ) && ! empty( $wp_block->context ) ) { @@ -754,12 +797,16 @@ function acf_block_render_template( $block, $content, $is_preview, $post_id, $wp $path = locate_template( $block['render_template'] ); } + do_action( 'acf/blocks/pre_block_template_render', $block, $content, $is_preview, $post_id, $wp_block, $context ); + // Include template. if ( file_exists( $path ) ) { include $path; } elseif ( $is_preview ) { echo acf_esc_html( apply_filters( 'acf/blocks/template_not_found_message', '

' . __( 'The render template for this ACF Block was not found', 'secure-custom-fields' ) . '

' ) ); } + + do_action( 'acf/blocks/post_block_template_render', $block, $content, $is_preview, $post_id, $wp_block, $context ); } /** @@ -813,7 +860,7 @@ function acf_enqueue_block_assets() { 'Change content alignment' => __( 'Change content alignment', 'secure-custom-fields' ), 'Error previewing block' => __( 'An error occurred when loading the preview for this block.', 'secure-custom-fields' ), 'Error loading block form' => __( 'An error occurred when loading the block in edit mode.', 'secure-custom-fields' ), - + 'Edit Block' => __( 'Edit Block', 'secure-custom-fields' ), /* translators: %s: Block type title */ '%s settings' => __( '%s settings', 'secure-custom-fields' ), ) @@ -1036,8 +1083,15 @@ function acf_ajax_fetch_block() { $content = ''; $is_preview = true; + $registry = WP_Block_Type_Registry::get_instance(); + $wp_block_type = $registry->get_registered( $block['name'] ); + + // We need to match what gets automatically passed to acf_rendered_block by WP core. + $wp_block = new stdClass(); + $wp_block->block_type = $wp_block_type; + // Render and store HTML. - $response['preview'] = acf_rendered_block( $block, $content, $is_preview, $post_id, null, $context, true ); + $response['preview'] = acf_rendered_block( $block, $content, $is_preview, $post_id, $wp_block, $context, true ); } // Send response. diff --git a/includes/class-acf-internal-post-type.php b/includes/class-acf-internal-post-type.php index 8808511d..67925f8c 100644 --- a/includes/class-acf-internal-post-type.php +++ b/includes/class-acf-internal-post-type.php @@ -166,7 +166,7 @@ public function get_raw_post( $id = 0 ) { * @since ACF 6.1 * * @param integer|string $id The post ID, key, or name. - * @return WP_Post|bool The post object, or false on failure. + * @return WP_Post|boolean The post object, or false on failure. */ public function get_post_object( $id = 0 ) { if ( is_numeric( $id ) ) { diff --git a/includes/fields/class-acf-field-button-group.php b/includes/fields/class-acf-field-button-group.php index d1000551..36e480fc 100644 --- a/includes/fields/class-acf-field-button-group.php +++ b/includes/fields/class-acf-field-button-group.php @@ -67,10 +67,11 @@ public function render_field( $field ) { // append $buttons[] = array( - 'name' => $field['name'], - 'value' => $_value, - 'label' => $_label, - 'checked' => $checked, + 'name' => $field['name'], + 'value' => $_value, + 'label' => $_label, + 'checked' => $checked, + 'button_group' => true, ); } @@ -79,16 +80,29 @@ public function render_field( $field ) { $buttons[0]['checked'] = true; } + // Ensure roving tabindex when allow_null is enabled and no selection yet. + if ( $field['allow_null'] && null === $selected && ! empty( $buttons ) ) { + $buttons[0]['tabindex'] = '0'; + } + // div - $div = array( 'class' => 'acf-button-group' ); + $div = array( + 'class' => 'acf-button-group', + 'role' => 'radiogroup', + ); + + // Add aria-labelledby if field has an ID for proper screen reader announcement + if ( ! empty( $field['id'] ) ) { + $div['aria-labelledby'] = $field['id'] . '-label'; + } - if ( 'vertical' === acf_maybe_get( $field, 'layout' ) ) { + if ( 'vertical' === $field['layout'] ) { $div['class'] .= ' -vertical'; } - if ( acf_maybe_get( $field, 'class' ) ) { - $div['class'] .= ' ' . acf_maybe_get( $field, 'class' ); + if ( $field['class'] ) { + $div['class'] .= ' ' . $field['class']; } - if ( acf_maybe_get( $field, 'allow_null' ) ) { + if ( $field['allow_null'] ) { $div['data-allow_null'] = 1; } diff --git a/includes/fields/class-acf-field-checkbox.php b/includes/fields/class-acf-field-checkbox.php index 582672be..5935a041 100644 --- a/includes/fields/class-acf-field-checkbox.php +++ b/includes/fields/class-acf-field-checkbox.php @@ -80,8 +80,14 @@ function render_field( $field ) { $li = ''; $ul = array( 'class' => 'acf-checkbox-list', + 'role' => 'group', ); + // Add aria-labelledby if field has an ID for proper screen reader announcement + if ( ! empty( $field['id'] ) ) { + $ul['aria-labelledby'] = $field['id'] . '-label'; + } + // append to class $ul['class'] .= ' ' . ( 'horizontal' === acf_maybe_get( $field, 'layout' ) ? 'acf-hl' : 'acf-bl' ); $ul['class'] .= ' ' . acf_maybe_get( $field, 'class', '' ); diff --git a/includes/fields/class-acf-field-color_picker.php b/includes/fields/class-acf-field-color_picker.php index 98fd1e0f..fd3d6ed9 100644 --- a/includes/fields/class-acf-field-color_picker.php +++ b/includes/fields/class-acf-field-color_picker.php @@ -26,9 +26,12 @@ function initialize() { $this->doc_url = 'https://developer.wordpress.org/secure-custom-fields/features/fields/color-picker/'; $this->tutorial_url = 'https://developer.wordpress.org/secure-custom-fields/features/fields/color-picker/color-picker-tutorial/'; $this->defaults = array( - 'default_value' => '', - 'enable_opacity' => false, - 'return_format' => 'string', // 'string'|'array' + 'default_value' => '', + 'enable_opacity' => false, + 'custom_palette_source' => '', + 'palette_colors' => '', + 'show_color_wheel' => true, + 'return_format' => 'string', // Possible values: 'string' or 'array'. ); } @@ -106,7 +109,7 @@ function input_admin_enqueue_scripts() { * @since ACF 3.6 * @date 23/01/13 */ - function render_field( $field ) { + public function render_field( $field ) { $text_input = acf_get_sub_array( $field, array( 'id', 'class', 'name', 'value' ) ); $hidden_input = acf_get_sub_array( $field, array( 'name', 'value' ) ); $text_input['data-alpha-skip-debounce'] = true; @@ -116,9 +119,55 @@ function render_field( $field ) { $text_input['data-alpha-enabled'] = true; } + // Handle color palette when the theme supports theme.json. + if ( wp_theme_has_theme_json() ) { + // If the field was set to use themejson. + if ( 'themejson' === $field['custom_palette_source'] ) { + $text_input['data-acf-palette-type'] = 'custom'; + + // Get the palette (theme + custom). + $global_settings = wp_get_global_settings(); + $palette = $global_settings['color']['palette']['theme'] ?? array(); + + // Extract only the color values. + $color_values = array_map( + fn( $c ) => $c['color'] ?? null, + $palette + ); + + // Remove nulls (in case any entries are missing 'color') + $color_values = array_filter( $color_values ); + + $hex_string = implode( ',', $color_values ); + + $text_input['data-acf-palette-colors'] = $hex_string; + } elseif ( 'custom' === $field['custom_palette_source'] && ! empty( $field['palette_colors'] ) ) { + // If the field was set to use a custom palette. + $text_input['data-acf-palette-type'] = 'custom'; + $text_input['data-acf-palette-colors'] = $field['palette_colors']; + } elseif ( '' === $field['custom_palette_source'] && ! empty( $field['palette_colors'] ) ) { + // This state can happen if they switched from a classic theme to a themejson theme without resaving the field. + $text_input['data-acf-palette-type'] = 'custom'; + $text_input['data-acf-palette-colors'] = $field['palette_colors']; + } else { + // Fallback to use the default color palette for the iris color picker. + $text_input['data-acf-palette-type'] = 'default'; + } + // phpcs:disable Universal.ControlStructures.DisallowLonelyIf.Found + } else { + // Handle color palette for themes that do not support themejson. + if ( ! empty( $field['palette_colors'] ) ) { + $text_input['data-acf-palette-type'] = 'custom'; + $text_input['data-acf-palette-colors'] = $field['palette_colors']; + } else { + // Fallback to use the default color palette for the iris color picker. + $text_input['data-acf-palette-type'] = 'default'; + } + } + // html ?> -
+
@@ -179,6 +228,86 @@ function render_field_settings( $field ) { ); } + + /** + * Renders the field settings used in the "Presentation" tab. + * + * @since 6.0 + * + * @param array $field The field settings array. + * @return void + */ + public function render_field_presentation_settings( $field ) { + acf_render_field_setting( + $field, + array( + 'label' => __( 'Show Custom Palette', 'secure-custom-fields' ), + 'instructions' => '', + 'type' => 'true_false', + 'name' => 'show_custom_palette', + 'ui' => 1, + ) + ); + + $custom_palette_conditions = array( + 'field' => 'show_custom_palette', + 'operator' => '==', + 'value' => 1, + ); + + if ( wp_theme_has_theme_json() ) { + acf_render_field_setting( + $field, + array( + 'label' => __( 'Custom Palette Source', 'secure-custom-fields' ), + 'instructions' => '', + 'type' => 'radio', + 'name' => 'custom_palette_source', + 'layout' => 'vertical', + 'choices' => array( + 'custom' => __( 'Specify custom colors', 'secure-custom-fields' ), + 'themejson' => __( 'Use colors from theme.json', 'secure-custom-fields' ), + ), + 'conditions' => array( + 'field' => 'show_custom_palette', + 'operator' => '==', + 'value' => 1, + ), + ) + ); + + $custom_palette_conditions = array( + 'field' => 'custom_palette_source', + 'operator' => '==', + 'value' => 'custom', + ); + } + + acf_render_field_setting( + $field, + array( + 'label' => __( 'Custom Palette', 'secure-custom-fields' ), + 'instructions' => __( 'Use a custom color palette by entering comma separated hex or rgba values', 'secure-custom-fields' ), + 'type' => 'text', + 'name' => 'palette_colors', + 'conditions' => $custom_palette_conditions, + ) + ); + + acf_render_field_setting( + $field, + array( + 'label' => __( 'Show Color Wheel', 'secure-custom-fields' ), + 'instructions' => '', + 'type' => 'true_false', + 'name' => 'show_color_wheel', + 'default_value' => 1, + 'ui' => 1, + ) + ); + } + + /** * Format the value for use in templates. At this stage, the value has been loaded from the * database and is being returned by an API function such as get_field(), the_field(), etc. diff --git a/includes/fields/class-acf-field-file.php b/includes/fields/class-acf-field-file.php index 37fe0dad..59c40187 100644 --- a/includes/fields/class-acf-field-file.php +++ b/includes/fields/class-acf-field-file.php @@ -130,7 +130,7 @@ function render_field( $field ) { ) ); ?> -
+
@@ -148,14 +148,14 @@ function render_field( $field ) {

- - + + - +
- +
diff --git a/includes/fields/class-acf-field-icon_picker.php b/includes/fields/class-acf-field-icon_picker.php index d7746ad2..31b4bb17 100644 --- a/includes/fields/class-acf-field-icon_picker.php +++ b/includes/fields/class-acf-field-icon_picker.php @@ -334,11 +334,16 @@ public function input_admin_enqueue_scripts() { * @return boolean true If the value is valid, false if not. */ public function validate_value( $valid, $value, $field, $input ) { - // If the value is empty, return true. You're allowed to save nothing. + // If the value is empty and it's not required, return true. You're allowed to save nothing. if ( empty( $value ) && empty( $field['required'] ) ) { return true; } + // Validate required. + if ( $field['required'] && ( empty( $value ) || empty( $value['value'] ) ) ) { + return false; + } + // If the value is not an array, return $valid status. if ( ! is_array( $value ) ) { return $valid; diff --git a/includes/fields/class-acf-field-image.php b/includes/fields/class-acf-field-image.php index d437965c..560fe737 100644 --- a/includes/fields/class-acf-field-image.php +++ b/includes/fields/class-acf-field-image.php @@ -127,17 +127,17 @@ function render_field( $field ) { ) ); ?> -
+
/>
- - + + - +
- +

diff --git a/includes/fields/class-acf-field-radio.php b/includes/fields/class-acf-field-radio.php index f0100200..28d33385 100644 --- a/includes/fields/class-acf-field-radio.php +++ b/includes/fields/class-acf-field-radio.php @@ -57,10 +57,16 @@ function render_field( $field ) { 'class' => 'acf-radio-list', 'data-allow_null' => $field['allow_null'], 'data-other_choice' => $field['other_choice'], + 'role' => 'radiogroup', ); + // Add aria-labelledby if field has an ID for proper screen reader announcement + if ( ! empty( $field['id'] ) ) { + $ul['aria-labelledby'] = $field['id'] . '-label'; + } + // append to class - $ul['class'] .= ' ' . ( $field['layout'] == 'horizontal' ? 'acf-hl' : 'acf-bl' ); + $ul['class'] .= ' ' . ( 'horizontal' === $field['layout'] ? 'acf-hl' : 'acf-bl' ); $ul['class'] .= ' ' . $field['class']; // Determine selected value. diff --git a/includes/fields/class-acf-field-repeater.php b/includes/fields/class-acf-field-repeater.php index 31b1743b..443dcfff 100644 --- a/includes/fields/class-acf-field-repeater.php +++ b/includes/fields/class-acf-field-repeater.php @@ -1016,24 +1016,55 @@ public function get_field_name_from_input_name( $input_name ) { $name_parts = array(); foreach ( $field_keys as $field_key ) { - if ( ! acf_is_field_key( $field_key ) ) { - if ( 'acfcloneindex' === $field_key ) { - $name_parts[] = 'acfcloneindex'; - continue; - } + // Preserve acfcloneindex + if ( 'acfcloneindex' === $field_key ) { + $name_parts[] = 'acfcloneindex'; + continue; + } - $row_num = str_replace( 'row-', '', $field_key ); + // Handle row numbers (row-0, row-1, etc.) + if ( strpos( $field_key, 'row-' ) === 0 ) { + $row_num = substr( $field_key, 4 ); if ( is_numeric( $row_num ) ) { $name_parts[] = (int) $row_num; continue; } } - $field = acf_get_field( $field_key ); + // Handle compound keys (field_..._field_...) + $compound_keys = preg_split( '/_field_/', $field_key ); + if ( count( $compound_keys ) > 1 ) { + foreach ( $compound_keys as $i => $sub_key ) { + if ( $i > 0 ) { + $sub_key = 'field_' . $sub_key; + } + + // Seamless clone fields use compound keys which can be skipped. + $field = acf_get_field( $sub_key ); + if ( $field && 'clone' === $field['type'] && 'seamless' === $field['display'] ) { + continue; + } + + $name_parts[] = $field && ! empty( $field['name'] ) ? $field['name'] : $sub_key; + } + continue; + } + + // Handle standard field keys + if ( strpos( $field_key, 'field_' ) === 0 ) { + + // Skip clone fields with prefix_name disabled. + $field = acf_get_field( $field_key ); + if ( $field && 'clone' === $field['type'] && empty( $field['prefix_name'] ) ) { + continue; + } - if ( $field ) { - $name_parts[] = $field['name']; + $name_parts[] = $field && ! empty( $field['name'] ) ? $field['name'] : $field_key; + continue; } + + // Fallback: just add as is + $name_parts[] = $field_key; } return implode( '_', $name_parts ); @@ -1093,17 +1124,19 @@ public function ajax_get_rows() { * We have to swap out the field name with the one sent via JS, * as the repeater could be inside a subfield. */ - $field['name'] = $args['field_name']; + $field['name'] = $args['field_name']; + $field['prefix'] = $args['field_prefix']; + $field['value'] = acf_get_value( $post_id, $field ); - $field['value'] = acf_get_value( $post_id, $field ); + if ( $args['refresh'] ) { + $response['total_rows'] = (int) acf_get_metadata_by_field( $post_id, $field ); + } + + // Render the rows to be sent back via AJAX. $field = acf_prepare_field( $field ); $repeater_table = new ACF_Repeater_Table( $field ); $response['rows'] = $repeater_table->rows( true ); - if ( $args['refresh'] ) { - $response['total_rows'] = (int) acf_get_metadata( $post_id, $args['field_name'] ); - } - wp_send_json_success( $response ); } } diff --git a/includes/fields/class-acf-field-taxonomy.php b/includes/fields/class-acf-field-taxonomy.php index 84eeb94c..5f990f9d 100644 --- a/includes/fields/class-acf-field-taxonomy.php +++ b/includes/fields/class-acf-field-taxonomy.php @@ -576,7 +576,7 @@ public function render_field_checkbox( $field ) { ); // checkbox saves an array. - if ( $field['field_type'] == 'checkbox' ) { + if ( 'checkbox' === $field['field_type'] ) { $field['name'] .= '[]'; } @@ -601,9 +601,18 @@ public function render_field_checkbox( $field ) { $args = apply_filters( 'acf/fields/taxonomy/wp_list_categories/name=' . $field['_name'], $args, $field ); $args = apply_filters( 'acf/fields/taxonomy/wp_list_categories/key=' . $field['key'], $args, $field ); + // Build UL attributes for accessibility and consistency. + $ul = array( + 'class' => 'acf-checkbox-list acf-bl', + 'role' => 'radio' === $field['field_type'] ? 'radiogroup' : 'group', + ); + + if ( ! empty( $field['id'] ) ) { + $ul['aria-labelledby'] = $field['id'] . '-label'; + } ?>
-
    +
      >
diff --git a/includes/forms/WC_Order.php b/includes/forms/WC_Order.php index 9eb2a6c9..77161428 100644 --- a/includes/forms/WC_Order.php +++ b/includes/forms/WC_Order.php @@ -73,7 +73,6 @@ public function add_meta_boxes( $post_type, $post ) { if ( $field_groups ) { foreach ( $field_groups as $field_group ) { $id = "acf-{$field_group['key']}"; // acf-group_123 - $title = $field_group['title']; // Group 1 $context = $field_group['position']; // normal, side, acf_after_title $priority = 'core'; // high, core, default, low @@ -104,7 +103,7 @@ public function add_meta_boxes( $post_type, $post ) { // Add the meta box. add_meta_box( $id, - esc_html( $title ), + acf_esc_html( acf_get_field_group_title( $field_group ) ), array( $this, 'render_meta_box' ), $screen, $context, diff --git a/includes/forms/form-comment.php b/includes/forms/form-comment.php index 923c72d9..63dd3321 100644 --- a/includes/forms/form-comment.php +++ b/includes/forms/form-comment.php @@ -148,7 +148,7 @@ function edit_comment( $comment ) { ?>
-

+

`); + } + + componentDidUpdate() { + this.setHTML(this.props.children); + } + + componentDidMount() { + this.setHTML(this.props.children); + } +} + +/** + * Gets the component type for a given node name + * Handles special cases like InnerBlocks, script tags, and comments + * + * @param {string} nodeName - Lowercase node name + * @returns {string|Function|null} - Component type or null + */ +function getComponentType(nodeName) { + switch (nodeName) { + case 'innerblocks': + return 'ACFInnerBlocks'; + case 'script': + return ScriptComponent; + case '#comment': + return null; + default: + return getJSXNameReplacement(nodeName); + } +} + +/** + * ACF InnerBlocks wrapper component + * Provides a container for WordPress InnerBlocks with proper props + * + * @param {Object} props - Component props + * @returns {JSX.Element} - Wrapped InnerBlocks component + */ +function ACFInnerBlocksComponent(props) { + const { className = 'acf-innerblocks-container' } = props; + const innerBlocksProps = useInnerBlocksProps({ className }, props); + + return jsx('div', { + ...innerBlocksProps, + children: innerBlocksProps.children, + }); +} + +/** + * Parses and transforms a DOM attribute to React props format + * Handles special cases: class -> className, style string -> style object, JSON values, booleans + * + * @param {Attr} attribute - DOM attribute object with name and value + * @returns {Object} - Transformed attribute {name, value} + */ +function parseAttribute(attribute) { + let attrName = attribute.name; + let attrValue = attribute.value; + + // Allow custom filtering via ACF hooks + const customParsed = acf.applyFilters( + 'acf_blocks_parse_node_attr', + false, + attribute + ); + if (customParsed) return customParsed; + + switch (attrName) { + case 'class': + // Convert HTML class to React className + attrName = 'className'; + break; + + case 'style': + // Parse inline CSS string to JavaScript style object + const styleObject = {}; + attrValue.split(';').forEach((declaration) => { + const colonIndex = declaration.indexOf(':'); + if (colonIndex > 0) { + let property = declaration.substr(0, colonIndex).trim(); + const value = declaration.substr(colonIndex + 1).trim(); + + // Convert kebab-case to camelCase (except CSS variables starting with -) + if (property.charAt(0) !== '-') { + property = acf.strCamelCase(property); + } + + styleObject[property] = value; + } + }); + attrValue = styleObject; + break; + + default: + // Preserve data- attributes as-is + if (attrName.indexOf('data-') === 0) break; + + // Apply JSX name transformations (e.g., onclick -> onClick) + attrName = getJSXNameReplacement(attrName); + + // Parse JSON array/object values + const firstChar = attrValue.charAt(0); + if (firstChar === '[' || firstChar === '{') { + attrValue = JSON.parse(attrValue); + } + + // Convert string booleans to actual booleans + if (attrValue === 'true' || attrValue === 'false') { + attrValue = attrValue === 'true'; + } + } + + return { name: attrName, value: attrValue }; +} + +/** + * Recursively parses a DOM node and converts it to React/JSX elements + * + * @param {Node} node - The DOM node to parse + * @param {number} depth - Current recursion depth (0-based) + * @returns {JSX.Element|null} - React element or null if node should be skipped + */ +function parseNodeToJSX(node, depth = 0) { + // Determine the component type for this node + const componentType = getComponentType(node.nodeName.toLowerCase()); + + if (!componentType) return null; + + const props = {}; + + // Add ref to first-level elements (except ACFInnerBlocks) + if (depth === 1 && componentType !== 'ACFInnerBlocks') { + props.ref = createRef(); + } + + // Parse all attributes and add to props + acf.arrayArgs(node.attributes) + .map(parseAttribute) + .forEach(({ name, value }) => { + props[name] = value; + }); + + // Handle special ACFInnerBlocks component + if (componentType === 'ACFInnerBlocks') { + return jsx(ACFInnerBlocksComponent, { ...props }); + } + + // Build element array: [type, props, ...children] + const elementArray = [componentType, props]; + + // Recursively process child nodes + acf.arrayArgs(node.childNodes).forEach((childNode) => { + if (childNode instanceof Text) { + const textContent = childNode.textContent; + if (textContent) { + elementArray.push(textContent); + } + } else { + elementArray.push(parseNodeToJSX(childNode, depth + 1)); + } + }); + + // Create and return React element + return createElement.apply(this, elementArray); +} + +/** + * Main parseJSX function exposed on the acf global object + * Converts HTML string to React elements for use in ACF blocks + * + * @param {string} htmlString - HTML markup to parse + * @returns {Array|JSX.Element} - React children from parsed HTML + */ +export function parseJSX(htmlString) { + // Wrap in div to ensure valid HTML structure + htmlString = '
' + htmlString + '
'; + + // Handle self-closing InnerBlocks tags (not valid HTML, but used in ACF) + htmlString = htmlString.replace( + /]+)?\/>/, + '' + ); + + // Parse with jQuery, convert to React, and extract children from wrapper div + const parsedElement = parseNodeToJSX(jQuery(htmlString)[0], 0); + return parsedElement.props.children; +} + +// Expose parseJSX function on acf global object for backward compatibility +acf.parseJSX = parseJSX; diff --git a/assets/src/js/pro/blocks-v3/high-order-components/with-align-content.js b/assets/src/js/pro/blocks-v3/high-order-components/with-align-content.js new file mode 100644 index 00000000..a91dfe67 --- /dev/null +++ b/assets/src/js/pro/blocks-v3/high-order-components/with-align-content.js @@ -0,0 +1,116 @@ +/** + * withAlignContent Higher-Order Component + * Adds content alignment toolbar controls to ACF blocks + * Supports both vertical alignment and matrix alignment (horizontal + vertical) + */ + +const { Fragment, Component } = wp.element; +const { BlockControls, BlockVerticalAlignmentToolbar } = wp.blockEditor; + +// Matrix alignment control (experimental) +const BlockAlignmentMatrixControl = + wp.blockEditor.__experimentalBlockAlignmentMatrixControl || + wp.blockEditor.BlockAlignmentMatrixControl; + +const BlockAlignmentMatrixToolbar = + wp.blockEditor.__experimentalBlockAlignmentMatrixToolbar || + wp.blockEditor.BlockAlignmentMatrixToolbar; + +/** + * Normalizes vertical alignment value + * + * @param {string} alignment - Alignment value + * @returns {string} - Normalized alignment (top, center, or bottom) + */ +const normalizeVerticalAlignment = (alignment) => { + return ['top', 'center', 'bottom'].includes(alignment) ? alignment : 'top'; +}; + +/** + * Gets the default horizontal alignment based on RTL setting + * + * @param {string} alignment - Current alignment value + * @returns {string} - Normalized alignment value (left, center, or right) + */ +const getDefaultHorizontalAlignment = (alignment) => { + const defaultAlign = acf.get('rtl') ? 'right' : 'left'; + return ['left', 'center', 'right'].includes(alignment) + ? alignment + : defaultAlign; +}; + +/** + * Normalizes matrix alignment value (vertical + horizontal) + * Format: "top left", "center center", etc. + * + * @param {string} alignment - Alignment value + * @returns {string} - Normalized matrix alignment + */ +const normalizeMatrixAlignment = (alignment) => { + if (alignment) { + const [vertical, horizontal] = alignment.split(' '); + return `${normalizeVerticalAlignment(vertical)} ${getDefaultHorizontalAlignment(horizontal)}`; + } + return 'center center'; +}; + +/** + * Higher-order component that adds content alignment controls + * Supports either vertical-only or matrix (2D) alignment based on block config + * + * @param {React.Component} BlockComponent - The component to wrap + * @param {Object} blockConfig - ACF block configuration + * @returns {React.Component} - Enhanced component with content alignment controls + */ +export const withAlignContent = (BlockComponent, blockConfig) => { + let AlignmentControl; + let normalizeAlignment; + + // Determine which alignment control to use based on block supports + if ( + blockConfig.supports.align_content === 'matrix' || + blockConfig.supports.alignContent === 'matrix' + ) { + // Use matrix control (horizontal + vertical) + AlignmentControl = + BlockAlignmentMatrixControl || BlockAlignmentMatrixToolbar; + normalizeAlignment = normalizeMatrixAlignment; + } else { + // Use vertical-only control + AlignmentControl = BlockVerticalAlignmentToolbar; + normalizeAlignment = normalizeVerticalAlignment; + } + + // If alignment control is not available, return original component + if (AlignmentControl === undefined) { + return BlockComponent; + } + + // Set default alignment on block config + blockConfig.alignContent = normalizeAlignment(blockConfig.alignContent); + + return class extends Component { + render() { + const { attributes, setAttributes } = this.props; + const { alignContent } = attributes; + + return ( + + + + + + + ); + } + }; +}; diff --git a/assets/src/js/pro/blocks-v3/high-order-components/with-align-text.js b/assets/src/js/pro/blocks-v3/high-order-components/with-align-text.js new file mode 100644 index 00000000..f54f5fba --- /dev/null +++ b/assets/src/js/pro/blocks-v3/high-order-components/with-align-text.js @@ -0,0 +1,58 @@ +/** + * withAlignText Higher-Order Component + * Adds text alignment toolbar controls to ACF blocks + */ + +const { Fragment, Component } = wp.element; +const { BlockControls, AlignmentToolbar } = wp.blockEditor; + +/** + * Gets the default text alignment based on RTL setting + * + * @param {string} alignment - Current alignment value + * @returns {string} - Normalized alignment value (left, center, or right) + */ +const getDefaultAlignment = (alignment) => { + const defaultAlign = acf.get('rtl') ? 'right' : 'left'; + return ['left', 'center', 'right'].includes(alignment) + ? alignment + : defaultAlign; +}; + +/** + * Higher-order component that adds text alignment controls + * Wraps a block component and adds AlignmentToolbar to BlockControls + * + * @param {React.Component} BlockComponent - The component to wrap + * @param {Object} blockConfig - ACF block configuration + * @returns {React.Component} - Enhanced component with text alignment controls + */ +export const withAlignText = (BlockComponent, blockConfig) => { + const normalizeAlignment = getDefaultAlignment; + + // Set default alignment on block config + blockConfig.alignText = normalizeAlignment(blockConfig.alignText); + + return class extends Component { + render() { + const { attributes, setAttributes } = this.props; + const { alignText } = attributes; + + return ( + + + + + + + ); + } + }; +}; diff --git a/assets/src/js/pro/blocks-v3/high-order-components/with-full-height.js b/assets/src/js/pro/blocks-v3/high-order-components/with-full-height.js new file mode 100644 index 00000000..7bc539b1 --- /dev/null +++ b/assets/src/js/pro/blocks-v3/high-order-components/with-full-height.js @@ -0,0 +1,48 @@ +/** + * withFullHeight Higher-Order Component + * Adds full height toggle control to ACF blocks + */ + +const { Fragment, Component } = wp.element; +const { BlockControls } = wp.blockEditor; + +// Full height control (experimental) +const BlockFullHeightAlignmentControl = + wp.blockEditor.__experimentalBlockFullHeightAligmentControl || + wp.blockEditor.__experimentalBlockFullHeightAlignmentControl || + wp.blockEditor.BlockFullHeightAlignmentControl; + +/** + * Higher-order component that adds full height toggle controls + * Allows blocks to expand to full available height + * + * @param {React.Component} BlockComponent - The component to wrap + * @returns {React.Component} - Enhanced component with full height controls + */ +export const withFullHeight = (BlockComponent) => { + // If control is not available, return original component + if (!BlockFullHeightAlignmentControl) { + return BlockComponent; + } + + return class extends Component { + render() { + const { attributes, setAttributes } = this.props; + const { fullHeight } = attributes; + + return ( + + + + + + + ); + } + }; +}; diff --git a/assets/src/js/pro/blocks-v3/register-block-type-v3.js b/assets/src/js/pro/blocks-v3/register-block-type-v3.js new file mode 100644 index 00000000..d310efc1 --- /dev/null +++ b/assets/src/js/pro/blocks-v3/register-block-type-v3.js @@ -0,0 +1,358 @@ +/** + * ACF Block Type Registration - Version 3 + * Handles registration of ACF blocks (version 3) with WordPress Gutenberg + * Includes attribute setup, higher-order component composition, and block filtering + */ + +import jQuery from 'jquery'; +import { BlockEdit } from './components/block-edit'; +import { withAlignText } from './high-order-components/with-align-text'; +import { withAlignContent } from './high-order-components/with-align-content'; +import { withFullHeight } from './high-order-components/with-full-height'; + +const { InnerBlocks } = wp.blockEditor; +const { Component } = wp.element; +const { createHigherOrderComponent } = wp.compose; + +// Registry to store registered block configurations +const registeredBlocks = {}; + +/** + * Adds an attribute to the block configuration + * + * @param {Object} attributes - Existing attributes object + * @param {string} attributeName - Name of the attribute to add + * @param {string} attributeType - Type of the attribute (string, boolean, etc.) + * @returns {Object} - Updated attributes object + */ +const addAttribute = (attributes, attributeName, attributeType) => { + attributes[attributeName] = { type: attributeType }; + return attributes; +}; + +/** + * Checks if block should be registered for current post type + * + * @param {Object} blockConfig - Block configuration + * @returns {boolean} - True if block should be registered + */ +function shouldRegisterBlock(blockConfig) { + const allowedPostTypes = blockConfig.post_types || []; + + if (allowedPostTypes.length) { + // Always allow in reusable blocks + allowedPostTypes.push('wp_block'); + + const currentPostType = acf.get('postType'); + if (!allowedPostTypes.includes(currentPostType)) { + return false; + } + } + + return true; +} + +/** + * Processes and normalizes block icon + * + * @param {Object} blockConfig - Block configuration + */ +function processBlockIcon(blockConfig) { + // Convert SVG string to JSX element + if ( + typeof blockConfig.icon === 'string' && + blockConfig.icon.substr(0, 4) === ' + ); + } + + // Remove icon if empty/invalid + if (!blockConfig.icon) { + delete blockConfig.icon; + } +} + +/** + * Validates and normalizes block category + * Falls back to 'common' if category doesn't exist + * + * @param {Object} blockConfig - Block configuration + */ +function validateBlockCategory(blockConfig) { + const categoryExists = wp.blocks + .getCategories() + .filter(({ slug }) => slug === blockConfig.category) + .pop(); + + if (!categoryExists) { + blockConfig.category = 'common'; + } +} + +/** + * Sets default values for block configuration + * + * @param {Object} blockConfig - Block configuration + * @returns {Object} - Block configuration with defaults applied + */ +function applyBlockDefaults(blockConfig) { + return acf.parseArgs(blockConfig, { + title: '', + name: '', + category: '', + api_version: 2, + acf_block_version: 3, + attributes: {}, + supports: {}, + }); +} + +/** + * Cleans up block attributes + * Removes empty default values + * + * @param {Object} blockConfig - Block configuration + */ +function cleanBlockAttributes(blockConfig) { + for (const attributeName in blockConfig.attributes) { + if ( + 'default' in blockConfig.attributes[attributeName] && + blockConfig.attributes[attributeName].default.length === 0 + ) { + delete blockConfig.attributes[attributeName].default; + } + } +} + +/** + * Configures anchor support if enabled + * + * @param {Object} blockConfig - Block configuration + */ +function configureAnchorSupport(blockConfig) { + if (blockConfig.supports && blockConfig.supports.anchor) { + blockConfig.attributes.anchor = { type: 'string' }; + } +} + +/** + * Applies higher-order components based on block supports + * + * @param {React.Component} EditComponent - Base edit component + * @param {Object} blockConfig - Block configuration + * @returns {React.Component} - Enhanced edit component + */ +function applyHigherOrderComponents(EditComponent, blockConfig) { + let enhancedComponent = EditComponent; + + // Add text alignment support + if (blockConfig.supports.alignText || blockConfig.supports.align_text) { + blockConfig.attributes = addAttribute( + blockConfig.attributes, + 'align_text', + 'string' + ); + enhancedComponent = withAlignText(enhancedComponent, blockConfig); + } + + // Add content alignment support + if ( + blockConfig.supports.alignContent || + blockConfig.supports.align_content + ) { + blockConfig.attributes = addAttribute( + blockConfig.attributes, + 'align_content', + 'string' + ); + enhancedComponent = withAlignContent(enhancedComponent, blockConfig); + } + + // Add full height support + if (blockConfig.supports.fullHeight || blockConfig.supports.full_height) { + blockConfig.attributes = addAttribute( + blockConfig.attributes, + 'full_height', + 'boolean' + ); + enhancedComponent = withFullHeight(enhancedComponent); + } + + return enhancedComponent; +} + +/** + * Registers an ACF block type (version 3) with WordPress + * + * @param {Object} blockConfig - ACF block configuration object + * @returns {Object|boolean} - Registered block type or false if not registered + */ +function registerACFBlockType(blockConfig) { + // Check if block should be registered for current post type + if (!shouldRegisterBlock(blockConfig)) { + return false; + } + + // Process icon + processBlockIcon(blockConfig); + + // Validate category + validateBlockCategory(blockConfig); + + // Apply default values + blockConfig = applyBlockDefaults(blockConfig); + + // Clean up attributes + cleanBlockAttributes(blockConfig); + + // Configure anchor support + configureAnchorSupport(blockConfig); + + // Start with base BlockEdit component + let EditComponent = BlockEdit; + + // Apply higher-order components based on supports + EditComponent = applyHigherOrderComponents(EditComponent, blockConfig); + + // Create edit function that passes blockConfig and jQuery + blockConfig.edit = function (props) { + return ; + }; + + // Create save function (ACF blocks save to post content as HTML comments) + blockConfig.save = () => ; + + // Store in registry + registeredBlocks[blockConfig.name] = blockConfig; + + // Register with WordPress + const registeredBlockType = wp.blocks.registerBlockType( + blockConfig.name, + blockConfig + ); + + // Ensure anchor attribute is properly configured + if ( + registeredBlockType && + registeredBlockType.attributes && + registeredBlockType.attributes.anchor + ) { + registeredBlockType.attributes.anchor = { type: 'string' }; + } + + return registeredBlockType; +} + +/** + * Retrieves a registered block configuration by name + * + * @param {string} blockName - Name of the block + * @returns {Object|boolean} - Block configuration or false + */ +function getRegisteredBlock(blockName) { + return registeredBlocks[blockName] || false; +} + +/** + * Higher-order component to migrate legacy attribute names to new format + * Handles backward compatibility for align_text -> alignText, etc. + */ +const withDefaultAttributes = createHigherOrderComponent( + (BlockListBlock) => + class extends Component { + constructor(props) { + super(props); + + const { name, attributes } = this.props; + const blockConfig = getRegisteredBlock(name); + + if (!blockConfig) return; + + // Remove empty string attributes + Object.keys(attributes).forEach((key) => { + if (attributes[key] === '') { + delete attributes[key]; + } + }); + + // Map old attribute names to new camelCase names + const attributeMap = { + full_height: 'fullHeight', + align_content: 'alignContent', + align_text: 'alignText', + }; + + Object.keys(attributeMap).forEach((oldKey) => { + const newKey = attributeMap[oldKey]; + + if (attributes[oldKey] !== undefined) { + // Migrate old key to new key + attributes[newKey] = attributes[oldKey]; + } else if ( + attributes[newKey] === undefined && + blockConfig[oldKey] !== undefined + ) { + // Set default from block config if not present + attributes[newKey] = blockConfig[oldKey]; + } + + // Clean up old attribute names + delete blockConfig[oldKey]; + delete attributes[oldKey]; + }); + + // Apply default values from block config for missing attributes + for (let key in blockConfig.attributes) { + if ( + attributes[key] === undefined && + blockConfig[key] !== undefined + ) { + attributes[key] = blockConfig[key]; + } + } + } + + render() { + return ; + } + }, + 'withDefaultAttributes' +); + +/** + * Initialize ACF blocks on the 'prepare' action + * Registers all ACF blocks with version 3 or higher + */ +acf.addAction('prepare', function () { + // Ensure wp.blockEditor exists (backward compatibility) + if (!wp.blockEditor) { + wp.blockEditor = wp.editor; + } + + const blockTypes = acf.get('blockTypes'); + + if (blockTypes) { + blockTypes.forEach((blockType) => { + // Only register blocks with version 3 or higher + if (parseInt(blockType.acf_block_version) >= 3) { + registerACFBlockType(blockType); + } + }); + } +}); + +/** + * Register WordPress filter for attribute migration + * Ensures backward compatibility with legacy attribute names + */ +wp.hooks.addFilter( + 'editor.BlockListBlock', + 'acf/with-default-attributes', + withDefaultAttributes +); + +// Export for testing/external use +export { registerACFBlockType, getRegisteredBlock }; diff --git a/assets/src/js/pro/blocks-v3/utils/post-locking.js b/assets/src/js/pro/blocks-v3/utils/post-locking.js new file mode 100644 index 00000000..eae004df --- /dev/null +++ b/assets/src/js/pro/blocks-v3/utils/post-locking.js @@ -0,0 +1,45 @@ +/** + * WordPress post locking utilities for ACF blocks + * Handles locking/unlocking post saving during block operations + */ + +/** + * Locks post saving in the WordPress editor + * Used when block operations are in progress (like fetching data) + * + * @param {string} clientId - The block's client ID + */ +export const lockPostSaving = (clientId) => { + if (wp.data.dispatch('core/editor')) { + wp.data.dispatch('core/editor').lockPostSaving('acf/block/' + clientId); + } +}; + +/** + * Unlocks post saving in the WordPress editor + * Called when block operations are complete + * + * @param {string} clientId - The block's client ID + */ +export const unlockPostSaving = (clientId) => { + if (wp.data.dispatch('core/editor')) { + wp.data + .dispatch('core/editor') + .unlockPostSaving('acf/block/' + clientId); + } +}; + +/** + * Sorts an object's keys alphabetically + * Used for consistent object serialization and comparison + * + * @param {Object} obj - Object to sort + * @returns {Object} - New object with sorted keys + */ +export const sortObjectKeys = (obj) => + Object.keys(obj) + .sort() + .reduce((result, key) => { + result[key] = obj[key]; + return result; + }, {}); From 45dc933aa8870d5b30c1ed95d41fddae3e9ead89 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Mon, 3 Nov 2025 17:42:05 +0100 Subject: [PATCH 12/22] V3 blocks loading correctly --- assets/src/js/pro/_acf-blocks.js | 7 ++++++- assets/src/js/pro/blocks-v3/components/jsx-parser.js | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/assets/src/js/pro/_acf-blocks.js b/assets/src/js/pro/_acf-blocks.js index 9b232471..a120f056 100644 --- a/assets/src/js/pro/_acf-blocks.js +++ b/assets/src/js/pro/_acf-blocks.js @@ -1656,7 +1656,12 @@ const md5 = require( 'md5' ); // Register block types. const blockTypes = acf.get( 'blockTypes' ); if ( blockTypes ) { - blockTypes.map( registerBlockType ); + // Only register blocks with version < 3 (v3 blocks are registered separately). + blockTypes + .filter( + ( blockType ) => parseInt( blockType.acf_block_version ) < 3 + ) + .map( registerBlockType ); } } diff --git a/assets/src/js/pro/blocks-v3/components/jsx-parser.js b/assets/src/js/pro/blocks-v3/components/jsx-parser.js index 3ef07066..b7c3c85e 100644 --- a/assets/src/js/pro/blocks-v3/components/jsx-parser.js +++ b/assets/src/js/pro/blocks-v3/components/jsx-parser.js @@ -5,7 +5,7 @@ import jQuery from 'jquery'; -const { jsx, createElement, createRef, Component } = wp.element; +const { createElement, createRef, Component } = wp.element; const useInnerBlocksProps = wp.blockEditor.__experimentalUseInnerBlocksProps || wp.blockEditor.useInnerBlocksProps; @@ -27,7 +27,7 @@ function getJSXNameReplacement(attrName) { */ class ScriptComponent extends Component { render() { - return jsx('div', { ref: (element) => (this.el = element) }); + return createElement('div', { ref: (element) => (this.el = element) }); } setHTML(scriptContent) { @@ -74,7 +74,7 @@ function ACFInnerBlocksComponent(props) { const { className = 'acf-innerblocks-container' } = props; const innerBlocksProps = useInnerBlocksProps({ className }, props); - return jsx('div', { + return createElement('div', { ...innerBlocksProps, children: innerBlocksProps.children, }); @@ -176,7 +176,7 @@ function parseNodeToJSX(node, depth = 0) { // Handle special ACFInnerBlocks component if (componentType === 'ACFInnerBlocks') { - return jsx(ACFInnerBlocksComponent, { ...props }); + return createElement(ACFInnerBlocksComponent, { ...props }); } // Build element array: [type, props, ...children] From 3f5d9660c9ad7d873e88b24b59fcd64d106bb0fe Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:18:34 +0100 Subject: [PATCH 13/22] Fix css not showing --- assets/src/sass/pro/acf-pro-input.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/src/sass/pro/acf-pro-input.scss b/assets/src/sass/pro/acf-pro-input.scss index 025897c5..63df30ee 100644 --- a/assets/src/sass/pro/acf-pro-input.scss +++ b/assets/src/sass/pro/acf-pro-input.scss @@ -1,4 +1,5 @@ @charset "UTF-8"; +@use './blocks'; /*-------------------------------------------------------------------------------------------- * * Vars From fd46fb318651c9db224661b1600b0b94bf5f733a Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:03:18 +0100 Subject: [PATCH 14/22] fix prettier --- .../js/pro/blocks-v3/components/block-edit.js | 393 +++++++++--------- .../js/pro/blocks-v3/components/block-form.js | 254 +++++------ .../blocks-v3/components/block-placeholder.js | 12 +- .../pro/blocks-v3/components/block-preview.js | 6 +- .../js/pro/blocks-v3/components/jsx-parser.js | 108 ++--- .../with-align-content.js | 44 +- .../high-order-components/with-align-text.js | 25 +- .../high-order-components/with-full-height.js | 14 +- .../pro/blocks-v3/register-block-type-v3.js | 158 +++---- .../js/pro/blocks-v3/utils/post-locking.js | 26 +- readme.txt | 2 +- 11 files changed, 538 insertions(+), 504 deletions(-) diff --git a/assets/src/js/pro/blocks-v3/components/block-edit.js b/assets/src/js/pro/blocks-v3/components/block-edit.js index b584e66f..5fb0c842 100644 --- a/assets/src/js/pro/blocks-v3/components/block-edit.js +++ b/assets/src/js/pro/blocks-v3/components/block-edit.js @@ -42,26 +42,26 @@ import { * @param {Object} props.blockType - ACF block type configuration * @returns {JSX.Element} - Rendered block editor */ -export const BlockEdit = (props) => { +export const BlockEdit = ( props ) => { const { attributes, setAttributes, context, isSelected, $, blockType } = props; const shouldValidate = blockType.validate; const { clientId } = useBlockEditContext(); - const [validationErrors, setValidationErrors] = useState(null); - const [showValidationErrors, setShowValidationErrors] = useState(null); - const [theSerializedAcfData, setTheSerializedAcfData] = useState(null); - const [blockFormHtml, setBlockFormHtml] = useState(''); - const [blockPreviewHtml, setBlockPreviewHtml] = useState( + const [ validationErrors, setValidationErrors ] = useState( null ); + const [ showValidationErrors, setShowValidationErrors ] = useState( null ); + const [ theSerializedAcfData, setTheSerializedAcfData ] = useState( null ); + const [ blockFormHtml, setBlockFormHtml ] = useState( '' ); + const [ blockPreviewHtml, setBlockPreviewHtml ] = useState( 'acf-block-preview-loading' ); - const [userHasInteractedWithForm, setUserHasInteractedWithForm] = - useState(false); + const [ userHasInteractedWithForm, setUserHasInteractedWithForm ] = + useState( false ); - const acfFormRef = useRef(null); - const previewRef = useRef(null); - const debounceRef = useRef(null); + const acfFormRef = useRef( null ); + const previewRef = useRef( null ); + const debounceRef = useRef( null ); /** * Fetches block data from server (form HTML, preview HTML, validation) @@ -72,16 +72,16 @@ export const BlockEdit = (props) => { * @param {Object} params.theContext - Block context * @param {boolean} params.isSelected - Whether block is selected */ - function fetchBlockData({ + function fetchBlockData( { theAttributes, theClientId, theContext, isSelected, - }) { - if (!theAttributes) return; + } ) { + if ( ! theAttributes ) return; // Generate hash of attributes for preload cache lookup - const attributesHash = generateAttributesHash(theAttributes, context); + const attributesHash = generateAttributesHash( theAttributes, context ); // Check for preloaded block data const preloadedData = checkPreloadedData( @@ -90,46 +90,48 @@ export const BlockEdit = (props) => { isSelected ); - if (preloadedData) { - handlePreloadedData(preloadedData); + if ( preloadedData ) { + handlePreloadedData( preloadedData ); return; } // Prepare query options const queryOptions = { preview: true, form: true, validate: true }; - if (!blockFormHtml) { + if ( ! blockFormHtml ) { queryOptions.validate = false; } - if (!shouldValidate) { + if ( ! shouldValidate ) { queryOptions.validate = false; } const blockData = { ...theAttributes }; - wp.data.dispatch('core/editor').lockPostSaving('acf-fetching-block'); + wp.data + .dispatch( 'core/editor' ) + .lockPostSaving( 'acf-fetching-block' ); // Fetch block data via AJAX - $.ajax({ - url: acf.get('ajaxurl'), + $.ajax( { + url: acf.get( 'ajaxurl' ), dataType: 'json', type: 'post', cache: false, - data: acf.prepareForAjax({ + data: acf.prepareForAjax( { action: 'acf/ajax/fetch-block', - block: JSON.stringify(blockData), + block: JSON.stringify( blockData ), clientId: theClientId, - context: JSON.stringify(theContext), + context: JSON.stringify( theContext ), query: queryOptions, - }), - }) - .done((response) => { + } ), + } ) + .done( ( response ) => { wp.data - .dispatch('core/editor') - .unlockPostSaving('acf-fetching-block'); + .dispatch( 'core/editor' ) + .unlockPostSaving( 'acf-fetching-block' ); - setBlockFormHtml(response.data.form); + setBlockFormHtml( response.data.form ); - if (response.data.preview) { + if ( response.data.preview ) { setBlockPreviewHtml( acf.applyFilters( 'blocks/preview/render', @@ -149,19 +151,19 @@ export const BlockEdit = (props) => { if ( response.data?.validation && - !response.data.validation.valid && + ! response.data.validation.valid && response.data.validation.errors ) { - setValidationErrors(response.data.validation.errors); + setValidationErrors( response.data.validation.errors ); } else { - setValidationErrors(null); + setValidationErrors( null ); } - }) - .fail(function () { + } ) + .fail( function () { wp.data - .dispatch('core/editor') - .unlockPostSaving('acf-fetching-block'); - }); + .dispatch( 'core/editor' ) + .unlockPostSaving( 'acf-fetching-block' ); + } ); } /** @@ -171,10 +173,10 @@ export const BlockEdit = (props) => { * @param {Object} ctx - Block context * @returns {string} - MD5 hash of serialized attributes */ - function generateAttributesHash(attrs, ctx) { + function generateAttributesHash( attrs, ctx ) { delete attrs.hasAcfError; - attrs._acf_context = sortObjectKeys(ctx); - return md5(JSON.stringify(sortObjectKeys(attrs))); + attrs._acf_context = sortObjectKeys( ctx ); + return md5( JSON.stringify( sortObjectKeys( attrs ) ) ); } /** @@ -185,35 +187,35 @@ export const BlockEdit = (props) => { * @param {boolean} selected - Whether block is selected * @returns {Object|boolean} - Preloaded data or false */ - function checkPreloadedData(hash, clientId, selected) { - if (selected) return false; + function checkPreloadedData( hash, clientId, selected ) { + if ( selected ) return false; - acf.debug('Preload check', hash, clientId); + acf.debug( 'Preload check', hash, clientId ); // Don't preload blocks inside Query Loop blocks - if (isInQueryLoop(clientId)) { + if ( isInQueryLoop( clientId ) ) { return false; } - const preloadedBlocks = acf.get('preloadedBlocks'); - if (!preloadedBlocks || !preloadedBlocks[hash]) { - acf.debug('Preload failed: not preloaded.'); + const preloadedBlocks = acf.get( 'preloadedBlocks' ); + if ( ! preloadedBlocks || ! preloadedBlocks[ hash ] ) { + acf.debug( 'Preload failed: not preloaded.' ); return false; } - const data = preloadedBlocks[hash]; + const data = preloadedBlocks[ hash ]; // Replace placeholder client ID with actual client ID - data.html = data.html.replaceAll(hash, clientId); + data.html = data.html.replaceAll( hash, clientId ); - if (data?.validation && data?.validation.errors) { - data.validation.errors = data.validation.errors.map((error) => { - error.input = error.input.replaceAll(hash, clientId); + if ( data?.validation && data?.validation.errors ) { + data.validation.errors = data.validation.errors.map( ( error ) => { + error.input = error.input.replaceAll( hash, clientId ); return error; - }); + } ); } - acf.debug('Preload successful', data); + acf.debug( 'Preload successful', data ); return data; } @@ -223,16 +225,16 @@ export const BlockEdit = (props) => { * @param {string} clientId - Block client ID * @returns {boolean} - True if inside Query Loop */ - function isInQueryLoop(clientId) { + function isInQueryLoop( clientId ) { const parentIds = wp.data - .select('core/block-editor') - .getBlockParents(clientId); + .select( 'core/block-editor' ) + .getBlockParents( clientId ); return ( wp.data - .select('core/block-editor') - .getBlocksByClientId(parentIds) - .filter((block) => block.name === 'core/query').length > 0 + .select( 'core/block-editor' ) + .getBlocksByClientId( parentIds ) + .filter( ( block ) => block.name === 'core/query' ).length > 0 ); } @@ -241,12 +243,12 @@ export const BlockEdit = (props) => { * * @param {Object} data - Preloaded data */ - function handlePreloadedData(data) { - if (data.form) { - setBlockFormHtml(data.html); - } else if (data.html) { + function handlePreloadedData( data ) { + if ( data.form ) { + setBlockFormHtml( data.html ); + } else if ( data.html ) { setBlockPreviewHtml( - acf.applyFilters('blocks/preview/render', data.html, true) + acf.applyFilters( 'blocks/preview/render', data.html, true ) ); } else { setBlockPreviewHtml( @@ -260,54 +262,54 @@ export const BlockEdit = (props) => { if ( data?.validation && - !data.validation.valid && + ! data.validation.valid && data.validation.errors ) { - setValidationErrors(data.validation.errors); + setValidationErrors( data.validation.errors ); } else { - setValidationErrors(null); + setValidationErrors( null ); } } // Initial fetch on mount and when selection changes - useEffect(() => { + useEffect( () => { function trackUserInteraction() { - setUserHasInteractedWithForm(true); - window.removeEventListener('click', trackUserInteraction); - window.removeEventListener('keydown', trackUserInteraction); + setUserHasInteractedWithForm( true ); + window.removeEventListener( 'click', trackUserInteraction ); + window.removeEventListener( 'keydown', trackUserInteraction ); } - fetchBlockData({ + fetchBlockData( { theAttributes: attributes, theClientId: clientId, theContext: context, isSelected: isSelected, - }); + } ); - window.addEventListener('click', trackUserInteraction); - window.addEventListener('keydown', trackUserInteraction); + window.addEventListener( 'click', trackUserInteraction ); + window.addEventListener( 'keydown', trackUserInteraction ); return () => { - window.removeEventListener('click', trackUserInteraction); - window.removeEventListener('keydown', trackUserInteraction); + window.removeEventListener( 'click', trackUserInteraction ); + window.removeEventListener( 'keydown', trackUserInteraction ); }; - }, []); + }, [] ); // Update hasAcfError attribute based on validation errors - useEffect(() => { + useEffect( () => { setAttributes( validationErrors ? { hasAcfError: true } : { hasAcfError: false } ); - }, [validationErrors, setAttributes]); + }, [ validationErrors, setAttributes ] ); // Listen for validation error events from other blocks - useEffect(() => { + useEffect( () => { const handleErrorEvent = () => { - lockPostSaving(clientId); - setShowValidationErrors(true); + lockPostSaving( clientId ); + setShowValidationErrors( true ); }; - document.addEventListener('acf/block/has-error', handleErrorEvent); + document.addEventListener( 'acf/block/has-error', handleErrorEvent ); return () => { document.removeEventListener( @@ -315,73 +317,74 @@ export const BlockEdit = (props) => { handleErrorEvent ); }; - }, []); + }, [] ); // Cleanup: unlock post saving on unmount useEffect( () => () => { - unlockPostSaving(props.clientId); + unlockPostSaving( props.clientId ); }, [] ); // Handle form data changes with debouncing - useEffect(() => { - clearTimeout(debounceRef.current); + useEffect( () => { + clearTimeout( debounceRef.current ); - debounceRef.current = setTimeout(() => { + debounceRef.current = setTimeout( () => { handleFormDataUpdate(); - }, 200); - }, [theSerializedAcfData]); + }, 200 ); + }, [ theSerializedAcfData ] ); /** * Updates block attributes when form data changes */ function handleFormDataUpdate() { - const parsedData = JSON.parse(theSerializedAcfData); - if (!parsedData) return; - if (theSerializedAcfData === JSON.stringify(attributes.data)) return; + const parsedData = JSON.parse( theSerializedAcfData ); + if ( ! parsedData ) return; + if ( theSerializedAcfData === JSON.stringify( attributes.data ) ) + return; const updatedAttributes = { ...attributes, data: { ...parsedData } }; - setAttributes(updatedAttributes); + setAttributes( updatedAttributes ); - fetchBlockData({ + fetchBlockData( { theAttributes: updatedAttributes, theClientId: clientId, theContext: context, isSelected: isSelected, - }); + } ); } // Trigger ACF actions when preview is rendered - useEffect(() => { - if (previewRef.current && blockPreviewHtml) { - const blockName = attributes.name.replace('acf/', ''); - const $preview = $(previewRef.current); + useEffect( () => { + if ( previewRef.current && blockPreviewHtml ) { + const blockName = attributes.name.replace( 'acf/', '' ); + const $preview = $( previewRef.current ); - acf.doAction('render_block_preview', $preview, attributes); + acf.doAction( 'render_block_preview', $preview, attributes ); acf.doAction( - `render_block_preview/type=${blockName}`, + `render_block_preview/type=${ blockName }`, $preview, attributes ); } - }, [blockPreviewHtml]); + }, [ blockPreviewHtml ] ); return ( ); }; @@ -390,7 +393,7 @@ export const BlockEdit = (props) => { * Inner component that handles rendering and portals * Separated to manage refs and portal targets properly */ -function BlockEditInner(props) { +function BlockEditInner( props ) { const { blockType, $, @@ -412,169 +415,169 @@ function BlockEditInner(props) { const { clientId } = useBlockEditContext(); const invisibleFormContainerRef = useRef(); const inspectorControlsRef = useRef(); - const [isModalOpen, setIsModalOpen] = useState(false); + const [ isModalOpen, setIsModalOpen ] = useState( false ); const modalFormContainerRef = useRef(); - const [currentFormContainer, setCurrentFormContainer] = useState(); + const [ currentFormContainer, setCurrentFormContainer ] = useState(); // Set current form container when modal opens - useEffect(() => { - if (isModalOpen && modalFormContainerRef?.current) { - setCurrentFormContainer(modalFormContainerRef.current); + useEffect( () => { + if ( isModalOpen && modalFormContainerRef?.current ) { + setCurrentFormContainer( modalFormContainerRef.current ); } - }, [isModalOpen, modalFormContainerRef]); + }, [ isModalOpen, modalFormContainerRef ] ); // Update form container when inspector panel is available - useEffect(() => { - if (isSelected && inspectorControlsRef?.current) { - setCurrentFormContainer(inspectorControlsRef.current); - } else if (isSelected && !inspectorControlsRef?.current) { + useEffect( () => { + if ( isSelected && inspectorControlsRef?.current ) { + setCurrentFormContainer( inspectorControlsRef.current ); + } else if ( isSelected && ! inspectorControlsRef?.current ) { // Wait for inspector to be available - setTimeout(() => { - setCurrentFormContainer(inspectorControlsRef.current); - }, 1); - } else if (!isSelected) { - setCurrentFormContainer(null); + setTimeout( () => { + setCurrentFormContainer( inspectorControlsRef.current ); + }, 1 ); + } else if ( ! isSelected ) { + setCurrentFormContainer( null ); } - }, [isSelected, inspectorControlsRef, inspectorControlsRef.current]); + }, [ isSelected, inspectorControlsRef, inspectorControlsRef.current ] ); // Build block CSS classes let blockClasses = 'acf-block-component acf-block-body'; blockClasses += ' acf-block-preview'; - if (validationErrors && showValidationErrors) { + if ( validationErrors && showValidationErrors ) { blockClasses += ' acf-block-has-validation-error'; } const blockProps = { - ...useBlockProps({ className: blockClasses, ref: previewRef }), + ...useBlockProps( { className: blockClasses, ref: previewRef } ), }; // Determine portal target let portalTarget = null; - if (currentFormContainer) { + if ( currentFormContainer ) { portalTarget = currentFormContainer; - } else if (inspectorControlsRef?.current) { + } else if ( inspectorControlsRef?.current ) { portalTarget = inspectorControlsRef.current; } return ( <> - {/* Block toolbar controls */} + { /* Block toolbar controls */ } { - setIsModalOpen(true); - }} + onClick={ () => { + setIsModalOpen( true ); + } } /> - {/* Inspector panel container */} + { /* Inspector panel container */ } -
+
- {/* Render form via portal when container is available */} - {portalTarget && + { /* Render form via portal when container is available */ } + { portalTarget && currentFormContainer && createPortal( <> { - blockFetcher({ + $={ $ } + clientId={ clientId } + blockFormHtml={ blockFormHtml } + onMount={ () => { + blockFetcher( { theAttributes: attributes, theClientId: clientId, theContext: context, isSelected: isSelected, - }); - }} - onChange={function ($form) { + } ); + } } + onChange={ function ( $form ) { const serializedData = acf.serialize( $form, - `acf-block_${clientId}` + `acf-block_${ clientId }` ); - if (serializedData) { + if ( serializedData ) { setTheSerializedAcfData( - JSON.stringify(serializedData) + JSON.stringify( serializedData ) ); } - }} - validationErrors={validationErrors} - showValidationErrors={showValidationErrors} - acfFormRef={acfFormRef} - theSerializedAcfData={theSerializedAcfData} + } } + validationErrors={ validationErrors } + showValidationErrors={ showValidationErrors } + acfFormRef={ acfFormRef } + theSerializedAcfData={ theSerializedAcfData } userHasInteractedWithForm={ userHasInteractedWithForm } setCurrentBlockFormContainer={ setCurrentFormContainer } - attributes={attributes} + attributes={ attributes } /> , currentFormContainer || inspectorControlsRef.current - )} + ) } - {/* Hidden container for form when not in inspector/modal */} + { /* Hidden container for form when not in inspector/modal */ } <>
- {/* Modal for editing block fields */} - {isModalOpen && ( + { /* Modal for editing block fields */ } + { isModalOpen && ( { - setCurrentFormContainer(null); - setIsModalOpen(false); - }} + isFullScreen={ true } + title={ blockType.title } + onRequestClose={ () => { + setCurrentFormContainer( null ); + setIsModalOpen( false ); + } } >
- )} + ) } - {/* Block preview */} + { /* Block preview */ } <> - {/* Show placeholder when no HTML */} - {blockPreviewHtml === 'acf-block-preview-no-html' ? ( + { /* Show placeholder when no HTML */ } + { blockPreviewHtml === 'acf-block-preview-no-html' ? ( - ) : null} + ) : null } - {/* Show spinner while loading */} - {blockPreviewHtml === 'acf-block-preview-loading' && ( + { /* Show spinner while loading */ } + { blockPreviewHtml === 'acf-block-preview-loading' && ( - )} + ) } - {/* Render actual preview HTML */} - {blockPreviewHtml !== 'acf-block-preview-loading' && + { /* Render actual preview HTML */ } + { blockPreviewHtml !== 'acf-block-preview-loading' && blockPreviewHtml !== 'acf-block-preview-no-html' && blockPreviewHtml && - acf.parseJSX(blockPreviewHtml)} + acf.parseJSX( blockPreviewHtml ) } diff --git a/assets/src/js/pro/blocks-v3/components/block-form.js b/assets/src/js/pro/blocks-v3/components/block-form.js index b15f5744..7baf6ec8 100644 --- a/assets/src/js/pro/blocks-v3/components/block-form.js +++ b/assets/src/js/pro/blocks-v3/components/block-form.js @@ -23,7 +23,7 @@ import { lockPostSaving, unlockPostSaving } from '../utils/post-locking'; * @param {Object} props.attributes - Block attributes * @returns {JSX.Element} - Rendered form component */ -export const BlockForm = ({ +export const BlockForm = ( { $, clientId, blockFormHtml, @@ -34,110 +34,118 @@ export const BlockForm = ({ acfFormRef, userHasInteractedWithForm, attributes, -}) => { - const [formHtml, setFormHtml] = useState(blockFormHtml); - const [pendingChange, setPendingChange] = useState(false); - const debounceTimer = useRef(null); - const [userInteracted, setUserInteracted] = useState(false); +} ) => { + const [ formHtml, setFormHtml ] = useState( blockFormHtml ); + const [ pendingChange, setPendingChange ] = useState( false ); + const debounceTimer = useRef( null ); + const [ userInteracted, setUserInteracted ] = useState( false ); // Call onMount when component first mounts - useEffect(() => { + useEffect( () => { onMount(); - }, []); + }, [] ); // Trigger onChange when there's a pending change and user has interacted - useEffect(() => { - if (pendingChange && (userHasInteractedWithForm || userInteracted)) { - onChange(pendingChange); - setPendingChange(false); + useEffect( () => { + if ( + pendingChange && + ( userHasInteractedWithForm || userInteracted ) + ) { + onChange( pendingChange ); + setPendingChange( false ); } - }, [pendingChange, userHasInteractedWithForm, setPendingChange, onChange]); + }, [ + pendingChange, + userHasInteractedWithForm, + setPendingChange, + onChange, + ] ); // Update form HTML when blockFormHtml prop changes - useEffect(() => { - if (!formHtml && blockFormHtml) { - setFormHtml(blockFormHtml); + useEffect( () => { + if ( ! formHtml && blockFormHtml ) { + setFormHtml( blockFormHtml ); } - }, [blockFormHtml]); + }, [ blockFormHtml ] ); // Handle validation errors - useEffect(() => { - if (!acfFormRef?.current) return; + useEffect( () => { + if ( ! acfFormRef?.current ) return; const validator = acf.getBlockFormValidator( - $(acfFormRef.current).find('.acf-block-fields') + $( acfFormRef.current ).find( '.acf-block-fields' ) ); validator.clearErrors(); - validator.set('notice', null); + validator.set( 'notice', null ); - acf.doAction('blocks/validation/pre_apply', validationErrors); + acf.doAction( 'blocks/validation/pre_apply', validationErrors ); - if (validationErrors) { - if (showValidationErrors) { - lockPostSaving(clientId); - validator.$el.find('.acf-notice').remove(); - validator.addErrors(validationErrors); - validator.showErrors('after'); + if ( validationErrors ) { + if ( showValidationErrors ) { + lockPostSaving( clientId ); + validator.$el.find( '.acf-notice' ).remove(); + validator.addErrors( validationErrors ); + validator.showErrors( 'after' ); } } else { // Handle successful validation if ( - validator.$el.find('.acf-notice').length > 0 && + validator.$el.find( '.acf-notice' ).length > 0 && showValidationErrors ) { - validator.$el.find('.acf-notice').remove(); - validator.addErrors([ - { message: acf.__('Validation successful') }, - ]); - validator.showErrors('after'); - validator.get('notice').update({ + validator.$el.find( '.acf-notice' ).remove(); + validator.addErrors( [ + { message: acf.__( 'Validation successful' ) }, + ] ); + validator.showErrors( 'after' ); + validator.get( 'notice' ).update( { type: 'success', - text: acf.__('Validation successful'), + text: acf.__( 'Validation successful' ), timeout: 1000, - }); - validator.set('notice', null); + } ); + validator.set( 'notice', null ); - setTimeout(() => { - validator.$el.find('.acf-notice').remove(); - }, 1001); + setTimeout( () => { + validator.$el.find( '.acf-notice' ).remove(); + }, 1001 ); - const noticeDispatch = wp.data.dispatch('core/notices'); + const noticeDispatch = wp.data.dispatch( 'core/notices' ); /** * Recursively checks for ACF errors in blocks * @param {Array} blocks - Array of block objects * @returns {Promise} - True if error found */ - function checkForErrors(blocks) { - return new Promise(function (resolve) { - blocks.forEach((block) => { - if (block.innerBlocks.length > 0) { - checkForErrors(block.innerBlocks).then( - (hasError) => { - if (hasError) return resolve(true); + function checkForErrors( blocks ) { + return new Promise( function ( resolve ) { + blocks.forEach( ( block ) => { + if ( block.innerBlocks.length > 0 ) { + checkForErrors( block.innerBlocks ).then( + ( hasError ) => { + if ( hasError ) return resolve( true ); } ); } - if (block.attributes.hasAcfError) { + if ( block.attributes.hasAcfError ) { const errorBlockClientId = block.clientId; - if (errorBlockClientId !== clientId) { + if ( errorBlockClientId !== clientId ) { wp.data - .dispatch('core/block-editor') - .selectBlock(errorBlockClientId); - return resolve(true); + .dispatch( 'core/block-editor' ) + .selectBlock( errorBlockClientId ); + return resolve( true ); } } - }); - return resolve(false); - }); + } ); + return resolve( false ); + } ); } checkForErrors( - wp.data.select('core/block-editor').getBlocks() - ).then((hasError) => { - if (hasError) { + wp.data.select( 'core/block-editor' ).getBlocks() + ).then( ( hasError ) => { + if ( hasError ) { noticeDispatch.createErrorNotice( acf.__( 'An ACF Block on this page requires attention before you can save.' @@ -145,67 +153,67 @@ export const BlockForm = ({ { id: 'acf-blocks-validation', isDismissible: true } ); } else { - noticeDispatch.removeNotice('acf-blocks-validation'); + noticeDispatch.removeNotice( 'acf-blocks-validation' ); } - }); + } ); } - unlockPostSaving(clientId); + unlockPostSaving( clientId ); } - acf.doAction('blocks/validation/post_apply', validationErrors); - }, [validationErrors, clientId, showValidationErrors]); + acf.doAction( 'blocks/validation/post_apply', validationErrors ); + }, [ validationErrors, clientId, showValidationErrors ] ); // Handle form remounting and change detection - useEffect(() => { - if (!acfFormRef?.current || !formHtml) return; + useEffect( () => { + if ( ! acfFormRef?.current || ! formHtml ) return; - acf.debug('Remounting ACF Form'); + acf.debug( 'Remounting ACF Form' ); const formElement = acfFormRef.current; - const $form = $(formElement); + const $form = $( formElement ); let isActive = true; - acf.doAction('remount', $form); + acf.doAction( 'remount', $form ); const handleChange = () => { - onChange($form); + onChange( $form ); }; const scheduleChange = () => { - if (!isActive) return; + if ( ! isActive ) return; - const inputs = formElement.querySelectorAll('input, textarea'); - const selects = formElement.querySelectorAll('select'); + const inputs = formElement.querySelectorAll( 'input, textarea' ); + const selects = formElement.querySelectorAll( 'select' ); - inputs.forEach((input) => { - input.removeEventListener('input', handleChange); - input.addEventListener('input', handleChange); - }); + inputs.forEach( ( input ) => { + input.removeEventListener( 'input', handleChange ); + input.addEventListener( 'input', handleChange ); + } ); - selects.forEach((select) => { - select.removeEventListener('change', handleChange); - select.addEventListener('change', handleChange); - }); + selects.forEach( ( select ) => { + select.removeEventListener( 'change', handleChange ); + select.addEventListener( 'change', handleChange ); + } ); - clearTimeout(debounceTimer.current); - debounceTimer.current = setTimeout(() => { - if (isActive) { - setPendingChange($form); + clearTimeout( debounceTimer.current ); + debounceTimer.current = setTimeout( () => { + if ( isActive ) { + setPendingChange( $form ); } - }, 200); + }, 200 ); }; // Observe DOM changes to detect field additions/removals - const domObserver = new MutationObserver(scheduleChange); + const domObserver = new MutationObserver( scheduleChange ); // Observe iframe content changes (for WYSIWYG editors) - const iframeObserver = new MutationObserver(() => { - if (isActive) { - setUserInteracted(true); + const iframeObserver = new MutationObserver( () => { + if ( isActive ) { + setUserInteracted( true ); scheduleChange(); } - }); + } ); const observerConfig = { attributes: true, @@ -214,55 +222,63 @@ export const BlockForm = ({ characterData: true, }; - domObserver.observe(formElement, observerConfig); + domObserver.observe( formElement, observerConfig ); // Watch for changes in iframes (WYSIWYG fields) - [...formElement.querySelectorAll('iframe')].forEach((iframe) => { - if (iframe && iframe.contentDocument) { + [ ...formElement.querySelectorAll( 'iframe' ) ].forEach( ( iframe ) => { + if ( iframe && iframe.contentDocument ) { const iframeBody = iframe.contentDocument.body; - if (iframeBody) { - iframeObserver.observe(iframeBody, observerConfig); + if ( iframeBody ) { + iframeObserver.observe( iframeBody, observerConfig ); } } - }); + } ); // Attach event listeners to form inputs - formElement.querySelectorAll('input, textarea').forEach((input) => { - input.addEventListener('input', handleChange); - }); + formElement + .querySelectorAll( 'input, textarea' ) + .forEach( ( input ) => { + input.addEventListener( 'input', handleChange ); + } ); - formElement.querySelectorAll('select').forEach((select) => { - select.addEventListener('change', handleChange); - }); + formElement.querySelectorAll( 'select' ).forEach( ( select ) => { + select.addEventListener( 'change', handleChange ); + } ); // Cleanup function return () => { isActive = false; domObserver.disconnect(); iframeObserver.disconnect(); - clearTimeout(debounceTimer.current); + clearTimeout( debounceTimer.current ); - if (formElement) { + if ( formElement ) { formElement - .querySelectorAll('input, textarea') - .forEach((input) => { - input.removeEventListener('input', handleChange); - }); - - formElement.querySelectorAll('select').forEach((select) => { - select.removeEventListener('change', handleChange); - }); + .querySelectorAll( 'input, textarea' ) + .forEach( ( input ) => { + input.removeEventListener( 'input', handleChange ); + } ); + + formElement + .querySelectorAll( 'select' ) + .forEach( ( select ) => { + select.removeEventListener( 'change', handleChange ); + } ); } }; - }, [acfFormRef, attributes, formHtml]); + }, [ acfFormRef, attributes, formHtml ] ); return (
); }; diff --git a/assets/src/js/pro/blocks-v3/components/block-placeholder.js b/assets/src/js/pro/blocks-v3/components/block-placeholder.js index fdae3d3a..3d005207 100644 --- a/assets/src/js/pro/blocks-v3/components/block-placeholder.js +++ b/assets/src/js/pro/blocks-v3/components/block-placeholder.js @@ -32,15 +32,15 @@ const blockIcon = ( * @param {string} props.blockLabel - The block's title/label * @returns {JSX.Element} - Rendered placeholder */ -export const BlockPlaceholder = ({ setBlockFormModalOpen, blockLabel }) => ( - } label={blockLabel}> +export const BlockPlaceholder = ( { setBlockFormModalOpen, blockLabel } ) => ( + } label={ blockLabel }> ); diff --git a/assets/src/js/pro/blocks-v3/components/block-preview.js b/assets/src/js/pro/blocks-v3/components/block-preview.js index 467a3b34..e8257953 100644 --- a/assets/src/js/pro/blocks-v3/components/block-preview.js +++ b/assets/src/js/pro/blocks-v3/components/block-preview.js @@ -13,8 +13,8 @@ * @param {Object} props.blockProps - Block props from useBlockProps hook * @returns {JSX.Element} - Rendered preview wrapper */ -export const BlockPreview = ({ children, blockPreviewHtml, blockProps }) => ( -
- {children} +export const BlockPreview = ( { children, blockPreviewHtml, blockProps } ) => ( +
+ { children }
); diff --git a/assets/src/js/pro/blocks-v3/components/jsx-parser.js b/assets/src/js/pro/blocks-v3/components/jsx-parser.js index b7c3c85e..a0e60e70 100644 --- a/assets/src/js/pro/blocks-v3/components/jsx-parser.js +++ b/assets/src/js/pro/blocks-v3/components/jsx-parser.js @@ -17,8 +17,8 @@ const useInnerBlocksProps = * @param {string} attrName - HTML attribute name * @returns {string} - JSX/React prop name */ -function getJSXNameReplacement(attrName) { - return acf.isget(acf, 'jsxNameReplacements', attrName) || attrName; +function getJSXNameReplacement( attrName ) { + return acf.isget( acf, 'jsxNameReplacements', attrName ) || attrName; } /** @@ -27,19 +27,21 @@ function getJSXNameReplacement(attrName) { */ class ScriptComponent extends Component { render() { - return createElement('div', { ref: (element) => (this.el = element) }); + return createElement( 'div', { + ref: ( element ) => ( this.el = element ), + } ); } - setHTML(scriptContent) { - jQuery(this.el).html(``); + setHTML( scriptContent ) { + jQuery( this.el ).html( `` ); } componentDidUpdate() { - this.setHTML(this.props.children); + this.setHTML( this.props.children ); } componentDidMount() { - this.setHTML(this.props.children); + this.setHTML( this.props.children ); } } @@ -50,8 +52,8 @@ class ScriptComponent extends Component { * @param {string} nodeName - Lowercase node name * @returns {string|Function|null} - Component type or null */ -function getComponentType(nodeName) { - switch (nodeName) { +function getComponentType( nodeName ) { + switch ( nodeName ) { case 'innerblocks': return 'ACFInnerBlocks'; case 'script': @@ -59,7 +61,7 @@ function getComponentType(nodeName) { case '#comment': return null; default: - return getJSXNameReplacement(nodeName); + return getJSXNameReplacement( nodeName ); } } @@ -70,14 +72,14 @@ function getComponentType(nodeName) { * @param {Object} props - Component props * @returns {JSX.Element} - Wrapped InnerBlocks component */ -function ACFInnerBlocksComponent(props) { +function ACFInnerBlocksComponent( props ) { const { className = 'acf-innerblocks-container' } = props; - const innerBlocksProps = useInnerBlocksProps({ className }, props); + const innerBlocksProps = useInnerBlocksProps( { className }, props ); - return createElement('div', { + return createElement( 'div', { ...innerBlocksProps, children: innerBlocksProps.children, - }); + } ); } /** @@ -87,7 +89,7 @@ function ACFInnerBlocksComponent(props) { * @param {Attr} attribute - DOM attribute object with name and value * @returns {Object} - Transformed attribute {name, value} */ -function parseAttribute(attribute) { +function parseAttribute( attribute ) { let attrName = attribute.name; let attrValue = attribute.value; @@ -97,9 +99,9 @@ function parseAttribute(attribute) { false, attribute ); - if (customParsed) return customParsed; + if ( customParsed ) return customParsed; - switch (attrName) { + switch ( attrName ) { case 'class': // Convert HTML class to React className attrName = 'className'; @@ -108,38 +110,38 @@ function parseAttribute(attribute) { case 'style': // Parse inline CSS string to JavaScript style object const styleObject = {}; - attrValue.split(';').forEach((declaration) => { - const colonIndex = declaration.indexOf(':'); - if (colonIndex > 0) { - let property = declaration.substr(0, colonIndex).trim(); - const value = declaration.substr(colonIndex + 1).trim(); + attrValue.split( ';' ).forEach( ( declaration ) => { + const colonIndex = declaration.indexOf( ':' ); + if ( colonIndex > 0 ) { + let property = declaration.substr( 0, colonIndex ).trim(); + const value = declaration.substr( colonIndex + 1 ).trim(); // Convert kebab-case to camelCase (except CSS variables starting with -) - if (property.charAt(0) !== '-') { - property = acf.strCamelCase(property); + if ( property.charAt( 0 ) !== '-' ) { + property = acf.strCamelCase( property ); } - styleObject[property] = value; + styleObject[ property ] = value; } - }); + } ); attrValue = styleObject; break; default: // Preserve data- attributes as-is - if (attrName.indexOf('data-') === 0) break; + if ( attrName.indexOf( 'data-' ) === 0 ) break; // Apply JSX name transformations (e.g., onclick -> onClick) - attrName = getJSXNameReplacement(attrName); + attrName = getJSXNameReplacement( attrName ); // Parse JSON array/object values - const firstChar = attrValue.charAt(0); - if (firstChar === '[' || firstChar === '{') { - attrValue = JSON.parse(attrValue); + const firstChar = attrValue.charAt( 0 ); + if ( firstChar === '[' || firstChar === '{' ) { + attrValue = JSON.parse( attrValue ); } // Convert string booleans to actual booleans - if (attrValue === 'true' || attrValue === 'false') { + if ( attrValue === 'true' || attrValue === 'false' ) { attrValue = attrValue === 'true'; } } @@ -154,48 +156,48 @@ function parseAttribute(attribute) { * @param {number} depth - Current recursion depth (0-based) * @returns {JSX.Element|null} - React element or null if node should be skipped */ -function parseNodeToJSX(node, depth = 0) { +function parseNodeToJSX( node, depth = 0 ) { // Determine the component type for this node - const componentType = getComponentType(node.nodeName.toLowerCase()); + const componentType = getComponentType( node.nodeName.toLowerCase() ); - if (!componentType) return null; + if ( ! componentType ) return null; const props = {}; // Add ref to first-level elements (except ACFInnerBlocks) - if (depth === 1 && componentType !== 'ACFInnerBlocks') { + if ( depth === 1 && componentType !== 'ACFInnerBlocks' ) { props.ref = createRef(); } // Parse all attributes and add to props - acf.arrayArgs(node.attributes) - .map(parseAttribute) - .forEach(({ name, value }) => { - props[name] = value; - }); + acf.arrayArgs( node.attributes ) + .map( parseAttribute ) + .forEach( ( { name, value } ) => { + props[ name ] = value; + } ); // Handle special ACFInnerBlocks component - if (componentType === 'ACFInnerBlocks') { - return createElement(ACFInnerBlocksComponent, { ...props }); + if ( componentType === 'ACFInnerBlocks' ) { + return createElement( ACFInnerBlocksComponent, { ...props } ); } // Build element array: [type, props, ...children] - const elementArray = [componentType, props]; + const elementArray = [ componentType, props ]; // Recursively process child nodes - acf.arrayArgs(node.childNodes).forEach((childNode) => { - if (childNode instanceof Text) { + acf.arrayArgs( node.childNodes ).forEach( ( childNode ) => { + if ( childNode instanceof Text ) { const textContent = childNode.textContent; - if (textContent) { - elementArray.push(textContent); + if ( textContent ) { + elementArray.push( textContent ); } } else { - elementArray.push(parseNodeToJSX(childNode, depth + 1)); + elementArray.push( parseNodeToJSX( childNode, depth + 1 ) ); } - }); + } ); // Create and return React element - return createElement.apply(this, elementArray); + return createElement.apply( this, elementArray ); } /** @@ -205,7 +207,7 @@ function parseNodeToJSX(node, depth = 0) { * @param {string} htmlString - HTML markup to parse * @returns {Array|JSX.Element} - React children from parsed HTML */ -export function parseJSX(htmlString) { +export function parseJSX( htmlString ) { // Wrap in div to ensure valid HTML structure htmlString = '
' + htmlString + '
'; @@ -216,7 +218,7 @@ export function parseJSX(htmlString) { ); // Parse with jQuery, convert to React, and extract children from wrapper div - const parsedElement = parseNodeToJSX(jQuery(htmlString)[0], 0); + const parsedElement = parseNodeToJSX( jQuery( htmlString )[ 0 ], 0 ); return parsedElement.props.children; } diff --git a/assets/src/js/pro/blocks-v3/high-order-components/with-align-content.js b/assets/src/js/pro/blocks-v3/high-order-components/with-align-content.js index a91dfe67..567123e1 100644 --- a/assets/src/js/pro/blocks-v3/high-order-components/with-align-content.js +++ b/assets/src/js/pro/blocks-v3/high-order-components/with-align-content.js @@ -22,8 +22,10 @@ const BlockAlignmentMatrixToolbar = * @param {string} alignment - Alignment value * @returns {string} - Normalized alignment (top, center, or bottom) */ -const normalizeVerticalAlignment = (alignment) => { - return ['top', 'center', 'bottom'].includes(alignment) ? alignment : 'top'; +const normalizeVerticalAlignment = ( alignment ) => { + return [ 'top', 'center', 'bottom' ].includes( alignment ) + ? alignment + : 'top'; }; /** @@ -32,9 +34,9 @@ const normalizeVerticalAlignment = (alignment) => { * @param {string} alignment - Current alignment value * @returns {string} - Normalized alignment value (left, center, or right) */ -const getDefaultHorizontalAlignment = (alignment) => { - const defaultAlign = acf.get('rtl') ? 'right' : 'left'; - return ['left', 'center', 'right'].includes(alignment) +const getDefaultHorizontalAlignment = ( alignment ) => { + const defaultAlign = acf.get( 'rtl' ) ? 'right' : 'left'; + return [ 'left', 'center', 'right' ].includes( alignment ) ? alignment : defaultAlign; }; @@ -46,10 +48,12 @@ const getDefaultHorizontalAlignment = (alignment) => { * @param {string} alignment - Alignment value * @returns {string} - Normalized matrix alignment */ -const normalizeMatrixAlignment = (alignment) => { - if (alignment) { - const [vertical, horizontal] = alignment.split(' '); - return `${normalizeVerticalAlignment(vertical)} ${getDefaultHorizontalAlignment(horizontal)}`; +const normalizeMatrixAlignment = ( alignment ) => { + if ( alignment ) { + const [ vertical, horizontal ] = alignment.split( ' ' ); + return `${ normalizeVerticalAlignment( + vertical + ) } ${ getDefaultHorizontalAlignment( horizontal ) }`; } return 'center center'; }; @@ -62,7 +66,7 @@ const normalizeMatrixAlignment = (alignment) => { * @param {Object} blockConfig - ACF block configuration * @returns {React.Component} - Enhanced component with content alignment controls */ -export const withAlignContent = (BlockComponent, blockConfig) => { +export const withAlignContent = ( BlockComponent, blockConfig ) => { let AlignmentControl; let normalizeAlignment; @@ -82,12 +86,12 @@ export const withAlignContent = (BlockComponent, blockConfig) => { } // If alignment control is not available, return original component - if (AlignmentControl === undefined) { + if ( AlignmentControl === undefined ) { return BlockComponent; } // Set default alignment on block config - blockConfig.alignContent = normalizeAlignment(blockConfig.alignContent); + blockConfig.alignContent = normalizeAlignment( blockConfig.alignContent ); return class extends Component { render() { @@ -98,17 +102,17 @@ export const withAlignContent = (BlockComponent, blockConfig) => { - + ); } diff --git a/assets/src/js/pro/blocks-v3/high-order-components/with-align-text.js b/assets/src/js/pro/blocks-v3/high-order-components/with-align-text.js index f54f5fba..27d6e353 100644 --- a/assets/src/js/pro/blocks-v3/high-order-components/with-align-text.js +++ b/assets/src/js/pro/blocks-v3/high-order-components/with-align-text.js @@ -12,9 +12,9 @@ const { BlockControls, AlignmentToolbar } = wp.blockEditor; * @param {string} alignment - Current alignment value * @returns {string} - Normalized alignment value (left, center, or right) */ -const getDefaultAlignment = (alignment) => { - const defaultAlign = acf.get('rtl') ? 'right' : 'left'; - return ['left', 'center', 'right'].includes(alignment) +const getDefaultAlignment = ( alignment ) => { + const defaultAlign = acf.get( 'rtl' ) ? 'right' : 'left'; + return [ 'left', 'center', 'right' ].includes( alignment ) ? alignment : defaultAlign; }; @@ -27,11 +27,11 @@ const getDefaultAlignment = (alignment) => { * @param {Object} blockConfig - ACF block configuration * @returns {React.Component} - Enhanced component with text alignment controls */ -export const withAlignText = (BlockComponent, blockConfig) => { +export const withAlignText = ( BlockComponent, blockConfig ) => { const normalizeAlignment = getDefaultAlignment; // Set default alignment on block config - blockConfig.alignText = normalizeAlignment(blockConfig.alignText); + blockConfig.alignText = normalizeAlignment( blockConfig.alignText ); return class extends Component { render() { @@ -42,15 +42,16 @@ export const withAlignText = (BlockComponent, blockConfig) => { - + ); } diff --git a/assets/src/js/pro/blocks-v3/high-order-components/with-full-height.js b/assets/src/js/pro/blocks-v3/high-order-components/with-full-height.js index 7bc539b1..50782b20 100644 --- a/assets/src/js/pro/blocks-v3/high-order-components/with-full-height.js +++ b/assets/src/js/pro/blocks-v3/high-order-components/with-full-height.js @@ -19,9 +19,9 @@ const BlockFullHeightAlignmentControl = * @param {React.Component} BlockComponent - The component to wrap * @returns {React.Component} - Enhanced component with full height controls */ -export const withFullHeight = (BlockComponent) => { +export const withFullHeight = ( BlockComponent ) => { // If control is not available, return original component - if (!BlockFullHeightAlignmentControl) { + if ( ! BlockFullHeightAlignmentControl ) { return BlockComponent; } @@ -34,13 +34,13 @@ export const withFullHeight = (BlockComponent) => { - + ); } diff --git a/assets/src/js/pro/blocks-v3/register-block-type-v3.js b/assets/src/js/pro/blocks-v3/register-block-type-v3.js index d310efc1..ba44bdb4 100644 --- a/assets/src/js/pro/blocks-v3/register-block-type-v3.js +++ b/assets/src/js/pro/blocks-v3/register-block-type-v3.js @@ -25,8 +25,8 @@ const registeredBlocks = {}; * @param {string} attributeType - Type of the attribute (string, boolean, etc.) * @returns {Object} - Updated attributes object */ -const addAttribute = (attributes, attributeName, attributeType) => { - attributes[attributeName] = { type: attributeType }; +const addAttribute = ( attributes, attributeName, attributeType ) => { + attributes[ attributeName ] = { type: attributeType }; return attributes; }; @@ -36,15 +36,15 @@ const addAttribute = (attributes, attributeName, attributeType) => { * @param {Object} blockConfig - Block configuration * @returns {boolean} - True if block should be registered */ -function shouldRegisterBlock(blockConfig) { +function shouldRegisterBlock( blockConfig ) { const allowedPostTypes = blockConfig.post_types || []; - if (allowedPostTypes.length) { + if ( allowedPostTypes.length ) { // Always allow in reusable blocks - allowedPostTypes.push('wp_block'); + allowedPostTypes.push( 'wp_block' ); - const currentPostType = acf.get('postType'); - if (!allowedPostTypes.includes(currentPostType)) { + const currentPostType = acf.get( 'postType' ); + if ( ! allowedPostTypes.includes( currentPostType ) ) { return false; } } @@ -57,20 +57,20 @@ function shouldRegisterBlock(blockConfig) { * * @param {Object} blockConfig - Block configuration */ -function processBlockIcon(blockConfig) { +function processBlockIcon( blockConfig ) { // Convert SVG string to JSX element if ( typeof blockConfig.icon === 'string' && - blockConfig.icon.substr(0, 4) === ' +
); } // Remove icon if empty/invalid - if (!blockConfig.icon) { + if ( ! blockConfig.icon ) { delete blockConfig.icon; } } @@ -81,13 +81,13 @@ function processBlockIcon(blockConfig) { * * @param {Object} blockConfig - Block configuration */ -function validateBlockCategory(blockConfig) { +function validateBlockCategory( blockConfig ) { const categoryExists = wp.blocks .getCategories() - .filter(({ slug }) => slug === blockConfig.category) + .filter( ( { slug } ) => slug === blockConfig.category ) .pop(); - if (!categoryExists) { + if ( ! categoryExists ) { blockConfig.category = 'common'; } } @@ -98,8 +98,8 @@ function validateBlockCategory(blockConfig) { * @param {Object} blockConfig - Block configuration * @returns {Object} - Block configuration with defaults applied */ -function applyBlockDefaults(blockConfig) { - return acf.parseArgs(blockConfig, { +function applyBlockDefaults( blockConfig ) { + return acf.parseArgs( blockConfig, { title: '', name: '', category: '', @@ -107,7 +107,7 @@ function applyBlockDefaults(blockConfig) { acf_block_version: 3, attributes: {}, supports: {}, - }); + } ); } /** @@ -116,13 +116,13 @@ function applyBlockDefaults(blockConfig) { * * @param {Object} blockConfig - Block configuration */ -function cleanBlockAttributes(blockConfig) { - for (const attributeName in blockConfig.attributes) { +function cleanBlockAttributes( blockConfig ) { + for ( const attributeName in blockConfig.attributes ) { if ( - 'default' in blockConfig.attributes[attributeName] && - blockConfig.attributes[attributeName].default.length === 0 + 'default' in blockConfig.attributes[ attributeName ] && + blockConfig.attributes[ attributeName ].default.length === 0 ) { - delete blockConfig.attributes[attributeName].default; + delete blockConfig.attributes[ attributeName ].default; } } } @@ -132,8 +132,8 @@ function cleanBlockAttributes(blockConfig) { * * @param {Object} blockConfig - Block configuration */ -function configureAnchorSupport(blockConfig) { - if (blockConfig.supports && blockConfig.supports.anchor) { +function configureAnchorSupport( blockConfig ) { + if ( blockConfig.supports && blockConfig.supports.anchor ) { blockConfig.attributes.anchor = { type: 'string' }; } } @@ -145,17 +145,17 @@ function configureAnchorSupport(blockConfig) { * @param {Object} blockConfig - Block configuration * @returns {React.Component} - Enhanced edit component */ -function applyHigherOrderComponents(EditComponent, blockConfig) { +function applyHigherOrderComponents( EditComponent, blockConfig ) { let enhancedComponent = EditComponent; // Add text alignment support - if (blockConfig.supports.alignText || blockConfig.supports.align_text) { + if ( blockConfig.supports.alignText || blockConfig.supports.align_text ) { blockConfig.attributes = addAttribute( blockConfig.attributes, 'align_text', 'string' ); - enhancedComponent = withAlignText(enhancedComponent, blockConfig); + enhancedComponent = withAlignText( enhancedComponent, blockConfig ); } // Add content alignment support @@ -168,17 +168,17 @@ function applyHigherOrderComponents(EditComponent, blockConfig) { 'align_content', 'string' ); - enhancedComponent = withAlignContent(enhancedComponent, blockConfig); + enhancedComponent = withAlignContent( enhancedComponent, blockConfig ); } // Add full height support - if (blockConfig.supports.fullHeight || blockConfig.supports.full_height) { + if ( blockConfig.supports.fullHeight || blockConfig.supports.full_height ) { blockConfig.attributes = addAttribute( blockConfig.attributes, 'full_height', 'boolean' ); - enhancedComponent = withFullHeight(enhancedComponent); + enhancedComponent = withFullHeight( enhancedComponent ); } return enhancedComponent; @@ -190,43 +190,49 @@ function applyHigherOrderComponents(EditComponent, blockConfig) { * @param {Object} blockConfig - ACF block configuration object * @returns {Object|boolean} - Registered block type or false if not registered */ -function registerACFBlockType(blockConfig) { +function registerACFBlockType( blockConfig ) { // Check if block should be registered for current post type - if (!shouldRegisterBlock(blockConfig)) { + if ( ! shouldRegisterBlock( blockConfig ) ) { return false; } // Process icon - processBlockIcon(blockConfig); + processBlockIcon( blockConfig ); // Validate category - validateBlockCategory(blockConfig); + validateBlockCategory( blockConfig ); // Apply default values - blockConfig = applyBlockDefaults(blockConfig); + blockConfig = applyBlockDefaults( blockConfig ); // Clean up attributes - cleanBlockAttributes(blockConfig); + cleanBlockAttributes( blockConfig ); // Configure anchor support - configureAnchorSupport(blockConfig); + configureAnchorSupport( blockConfig ); // Start with base BlockEdit component let EditComponent = BlockEdit; // Apply higher-order components based on supports - EditComponent = applyHigherOrderComponents(EditComponent, blockConfig); + EditComponent = applyHigherOrderComponents( EditComponent, blockConfig ); // Create edit function that passes blockConfig and jQuery - blockConfig.edit = function (props) { - return ; + blockConfig.edit = function ( props ) { + return ( + + ); }; // Create save function (ACF blocks save to post content as HTML comments) blockConfig.save = () => ; // Store in registry - registeredBlocks[blockConfig.name] = blockConfig; + registeredBlocks[ blockConfig.name ] = blockConfig; // Register with WordPress const registeredBlockType = wp.blocks.registerBlockType( @@ -252,8 +258,8 @@ function registerACFBlockType(blockConfig) { * @param {string} blockName - Name of the block * @returns {Object|boolean} - Block configuration or false */ -function getRegisteredBlock(blockName) { - return registeredBlocks[blockName] || false; +function getRegisteredBlock( blockName ) { + return registeredBlocks[ blockName ] || false; } /** @@ -261,22 +267,22 @@ function getRegisteredBlock(blockName) { * Handles backward compatibility for align_text -> alignText, etc. */ const withDefaultAttributes = createHigherOrderComponent( - (BlockListBlock) => + ( BlockListBlock ) => class extends Component { - constructor(props) { - super(props); + constructor( props ) { + super( props ); const { name, attributes } = this.props; - const blockConfig = getRegisteredBlock(name); + const blockConfig = getRegisteredBlock( name ); - if (!blockConfig) return; + if ( ! blockConfig ) return; // Remove empty string attributes - Object.keys(attributes).forEach((key) => { - if (attributes[key] === '') { - delete attributes[key]; + Object.keys( attributes ).forEach( ( key ) => { + if ( attributes[ key ] === '' ) { + delete attributes[ key ]; } - }); + } ); // Map old attribute names to new camelCase names const attributeMap = { @@ -285,38 +291,38 @@ const withDefaultAttributes = createHigherOrderComponent( align_text: 'alignText', }; - Object.keys(attributeMap).forEach((oldKey) => { - const newKey = attributeMap[oldKey]; + Object.keys( attributeMap ).forEach( ( oldKey ) => { + const newKey = attributeMap[ oldKey ]; - if (attributes[oldKey] !== undefined) { + if ( attributes[ oldKey ] !== undefined ) { // Migrate old key to new key - attributes[newKey] = attributes[oldKey]; + attributes[ newKey ] = attributes[ oldKey ]; } else if ( - attributes[newKey] === undefined && - blockConfig[oldKey] !== undefined + attributes[ newKey ] === undefined && + blockConfig[ oldKey ] !== undefined ) { // Set default from block config if not present - attributes[newKey] = blockConfig[oldKey]; + attributes[ newKey ] = blockConfig[ oldKey ]; } // Clean up old attribute names - delete blockConfig[oldKey]; - delete attributes[oldKey]; - }); + delete blockConfig[ oldKey ]; + delete attributes[ oldKey ]; + } ); // Apply default values from block config for missing attributes - for (let key in blockConfig.attributes) { + for ( let key in blockConfig.attributes ) { if ( - attributes[key] === undefined && - blockConfig[key] !== undefined + attributes[ key ] === undefined && + blockConfig[ key ] !== undefined ) { - attributes[key] = blockConfig[key]; + attributes[ key ] = blockConfig[ key ]; } } } render() { - return ; + return ; } }, 'withDefaultAttributes' @@ -326,23 +332,23 @@ const withDefaultAttributes = createHigherOrderComponent( * Initialize ACF blocks on the 'prepare' action * Registers all ACF blocks with version 3 or higher */ -acf.addAction('prepare', function () { +acf.addAction( 'prepare', function () { // Ensure wp.blockEditor exists (backward compatibility) - if (!wp.blockEditor) { + if ( ! wp.blockEditor ) { wp.blockEditor = wp.editor; } - const blockTypes = acf.get('blockTypes'); + const blockTypes = acf.get( 'blockTypes' ); - if (blockTypes) { - blockTypes.forEach((blockType) => { + if ( blockTypes ) { + blockTypes.forEach( ( blockType ) => { // Only register blocks with version 3 or higher - if (parseInt(blockType.acf_block_version) >= 3) { - registerACFBlockType(blockType); + if ( parseInt( blockType.acf_block_version ) >= 3 ) { + registerACFBlockType( blockType ); } - }); + } ); } -}); +} ); /** * Register WordPress filter for attribute migration diff --git a/assets/src/js/pro/blocks-v3/utils/post-locking.js b/assets/src/js/pro/blocks-v3/utils/post-locking.js index eae004df..f56a5693 100644 --- a/assets/src/js/pro/blocks-v3/utils/post-locking.js +++ b/assets/src/js/pro/blocks-v3/utils/post-locking.js @@ -9,9 +9,11 @@ * * @param {string} clientId - The block's client ID */ -export const lockPostSaving = (clientId) => { - if (wp.data.dispatch('core/editor')) { - wp.data.dispatch('core/editor').lockPostSaving('acf/block/' + clientId); +export const lockPostSaving = ( clientId ) => { + if ( wp.data.dispatch( 'core/editor' ) ) { + wp.data + .dispatch( 'core/editor' ) + .lockPostSaving( 'acf/block/' + clientId ); } }; @@ -21,11 +23,11 @@ export const lockPostSaving = (clientId) => { * * @param {string} clientId - The block's client ID */ -export const unlockPostSaving = (clientId) => { - if (wp.data.dispatch('core/editor')) { +export const unlockPostSaving = ( clientId ) => { + if ( wp.data.dispatch( 'core/editor' ) ) { wp.data - .dispatch('core/editor') - .unlockPostSaving('acf/block/' + clientId); + .dispatch( 'core/editor' ) + .unlockPostSaving( 'acf/block/' + clientId ); } }; @@ -36,10 +38,10 @@ export const unlockPostSaving = (clientId) => { * @param {Object} obj - Object to sort * @returns {Object} - New object with sorted keys */ -export const sortObjectKeys = (obj) => - Object.keys(obj) +export const sortObjectKeys = ( obj ) => + Object.keys( obj ) .sort() - .reduce((result, key) => { - result[key] = obj[key]; + .reduce( ( result, key ) => { + result[ key ] = obj[ key ]; return result; - }, {}); + }, {} ); diff --git a/readme.txt b/readme.txt index 80f0289c..5b931094 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: wordpressdotorg Tags: fields, custom fields, meta, scf Requires at least: 6.0 -Tested up to: 6.8 +Tested up to: 6.8.3 Requires PHP: 7.4 Stable tag: 6.5.7 License: GPLv2 or later From a5fe67d2f7d5d3c77ee8d025d68a247de8d0ecc9 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:08:40 +0100 Subject: [PATCH 15/22] Fix more prettier --- assets/src/js/_acf-condition-types.js | 867 ++++++++++++----------- assets/src/js/_acf-field-button-group.js | 78 +- assets/src/js/_acf-field-color-picker.js | 50 +- assets/src/js/_acf-field-radio.js | 48 +- assets/src/js/_acf-field-taxonomy.js | 214 +++--- assets/src/js/_acf-validation.js | 547 +++++++------- assets/src/js/_browse-fields-modal.js | 288 ++++---- assets/src/js/_field-group-field.js | 748 +++++++++---------- assets/src/js/pro/_acf-blocks.js | 416 ++++++++--- 9 files changed, 1761 insertions(+), 1495 deletions(-) diff --git a/assets/src/js/_acf-condition-types.js b/assets/src/js/_acf-condition-types.js index ffd676c7..9761bc52 100644 --- a/assets/src/js/_acf-condition-types.js +++ b/assets/src/js/_acf-condition-types.js @@ -1,12 +1,14 @@ -(function ($, undefined) { +( function ( $, undefined ) { const __ = acf.__; - const parseString = function (val) { + const parseString = function ( val ) { return val ? '' + val : ''; }; - const isEqualTo = function (v1, v2) { - return parseString(v1).toLowerCase() === parseString(v2).toLowerCase(); + const isEqualTo = function ( v1, v2 ) { + return ( + parseString( v1 ).toLowerCase() === parseString( v2 ).toLowerCase() + ); }; /** @@ -16,44 +18,44 @@ * @param {number|string|Array} v2 - The selected value to compare. * @returns {boolean} Returns true if the values are equal numbers, otherwise returns false. */ - const isEqualToNumber = function (v1, v2) { - if (v2 instanceof Array) { - return v2.length === 1 && isEqualToNumber(v1, v2[0]); + const isEqualToNumber = function ( v1, v2 ) { + if ( v2 instanceof Array ) { + return v2.length === 1 && isEqualToNumber( v1, v2[ 0 ] ); } - return parseFloat(v1) === parseFloat(v2); + return parseFloat( v1 ) === parseFloat( v2 ); }; - const isGreaterThan = function (v1, v2) { - return parseFloat(v1) > parseFloat(v2); + const isGreaterThan = function ( v1, v2 ) { + return parseFloat( v1 ) > parseFloat( v2 ); }; - const isLessThan = function (v1, v2) { - return parseFloat(v1) < parseFloat(v2); + const isLessThan = function ( v1, v2 ) { + return parseFloat( v1 ) < parseFloat( v2 ); }; - const inArray = function (v1, array) { + const inArray = function ( v1, array ) { // cast all values as string - array = array.map(function (v2) { - return parseString(v2); - }); + array = array.map( function ( v2 ) { + return parseString( v2 ); + } ); - return array.indexOf(v1) > -1; + return array.indexOf( v1 ) > -1; }; - const containsString = function (haystack, needle) { - return parseString(haystack).indexOf(parseString(needle)) > -1; + const containsString = function ( haystack, needle ) { + return parseString( haystack ).indexOf( parseString( needle ) ) > -1; }; - const matchesPattern = function (v1, pattern) { - const regexp = new RegExp(parseString(pattern), 'gi'); - return parseString(v1).match(regexp); + const matchesPattern = function ( v1, pattern ) { + const regexp = new RegExp( parseString( pattern ), 'gi' ); + return parseString( v1 ).match( regexp ); }; - const conditionalSelect2 = function (field, type) { - const $select = $(''); - let queryAction = `acf/fields/${type}/query`; + const conditionalSelect2 = function ( field, type ) { + const $select = $( '' ); + let queryAction = `acf/fields/${ type }/query`; - if (type === 'user') { + if ( type === 'user' ) { queryAction = 'acf/ajax/query_users'; } @@ -64,28 +66,28 @@ type: field.data.key, }; - const typeAttr = acf.escAttr(type); + const typeAttr = acf.escAttr( type ); - const template = function (selection) { + const template = function ( selection ) { return ( - `` + - acf.strEscape(selection.text) + + `` + + acf.strEscape( selection.text ) + '' ); }; - const resultsTemplate = function (results) { - let classes = results.text.startsWith('- ') - ? `acf-${typeAttr}-select-name acf-${typeAttr}-select-sub-item` - : `acf-${typeAttr}-select-name`; + const resultsTemplate = function ( results ) { + let classes = results.text.startsWith( '- ' ) + ? `acf-${ typeAttr }-select-name acf-${ typeAttr }-select-sub-item` + : `acf-${ typeAttr }-select-name`; return ( '' + - acf.strEscape(results.text) + + acf.strEscape( results.text ) + '' + - `` + - (results.id ? results.id : '') + + `` + + ( results.id ? results.id : '' ) + '' ); }; @@ -94,21 +96,23 @@ field: false, ajax: true, ajaxAction: queryAction, - ajaxData: function (data) { + ajaxData: function ( data ) { ajaxData.paged = data.paged; ajaxData.s = data.s; ajaxData.conditional_logic = true; - ajaxData.include = $.isNumeric(data.s) ? Number(data.s) : ''; - return acf.prepareForAjax(ajaxData); + ajaxData.include = $.isNumeric( data.s ) + ? Number( data.s ) + : ''; + return acf.prepareForAjax( ajaxData ); }, - escapeMarkup: function (markup) { - return acf.escHtml(markup); + escapeMarkup: function ( markup ) { + return acf.escHtml( markup ); }, templateSelection: template, templateResult: resultsTemplate, }; - $select.data('acfSelect2Props', select2Props); + $select.data( 'acfSelect2Props', select2Props ); return $select; }; /** @@ -116,721 +120,721 @@ * * @since ACF 6.3 */ - const HasPageLink = acf.Condition.extend({ + const HasPageLink = acf.Condition.extend( { type: 'hasPageLink', operator: '==', - label: __('Page is equal to'), - fieldTypes: ['page_link'], - match: function (rule, field) { - return isEqualTo(rule.value, field.val()); + label: __( 'Page is equal to' ), + fieldTypes: [ 'page_link' ], + match: function ( rule, field ) { + return isEqualTo( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'page_link'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'page_link' ); }, - }); + } ); - acf.registerConditionType(HasPageLink); + acf.registerConditionType( HasPageLink ); /** * Adds condition for Page Link not equal to. * * @since ACF 6.3 */ - const HasPageLinkNotEqual = acf.Condition.extend({ + const HasPageLinkNotEqual = acf.Condition.extend( { type: 'hasPageLinkNotEqual', operator: '!==', - label: __('Page is not equal to'), - fieldTypes: ['page_link'], - match: function (rule, field) { - return !isEqualTo(rule.value, field.val()); + label: __( 'Page is not equal to' ), + fieldTypes: [ 'page_link' ], + match: function ( rule, field ) { + return ! isEqualTo( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'page_link'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'page_link' ); }, - }); + } ); - acf.registerConditionType(HasPageLinkNotEqual); + acf.registerConditionType( HasPageLinkNotEqual ); /** * Adds condition for Page Link containing a specific Page Link. * * @since ACF 6.3 */ - const containsPageLink = acf.Condition.extend({ + const containsPageLink = acf.Condition.extend( { type: 'containsPageLink', operator: '==contains', - label: __('Pages contain'), - fieldTypes: ['page_link'], - match: function (rule, field) { + label: __( 'Pages contain' ), + fieldTypes: [ 'page_link' ], + match: function ( rule, field ) { const val = field.val(); const ruleVal = rule.value; let match = false; - if (val instanceof Array) { - match = val.includes(ruleVal); + if ( val instanceof Array ) { + match = val.includes( ruleVal ); } else { match = val === ruleVal; } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'page_link'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'page_link' ); }, - }); + } ); - acf.registerConditionType(containsPageLink); + acf.registerConditionType( containsPageLink ); /** * Adds condition for Page Link not containing a specific Page Link. * * @since ACF 6.3 */ - const containsNotPageLink = acf.Condition.extend({ + const containsNotPageLink = acf.Condition.extend( { type: 'containsNotPageLink', operator: '!=contains', - label: __('Pages do not contain'), - fieldTypes: ['page_link'], - match: function (rule, field) { + label: __( 'Pages do not contain' ), + fieldTypes: [ 'page_link' ], + match: function ( rule, field ) { const val = field.val(); const ruleVal = rule.value; let match = true; - if (val instanceof Array) { - match = !val.includes(ruleVal); + if ( val instanceof Array ) { + match = ! val.includes( ruleVal ); } else { match = val !== ruleVal; } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'page_link'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'page_link' ); }, - }); + } ); - acf.registerConditionType(containsNotPageLink); + acf.registerConditionType( containsNotPageLink ); /** * Adds condition for when any page link is selected. * * @since ACF 6.3 */ - const HasAnyPageLink = acf.Condition.extend({ + const HasAnyPageLink = acf.Condition.extend( { type: 'hasAnyPageLink', operator: '!=empty', - label: __('Has any page selected'), - fieldTypes: ['page_link'], - match: function (rule, field) { + label: __( 'Has any page selected' ), + fieldTypes: [ 'page_link' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !!val; + return !! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasAnyPageLink); + acf.registerConditionType( HasAnyPageLink ); /** * Adds condition for when no page link is selected. * * @since ACF 6.3 */ - const HasNoPageLink = acf.Condition.extend({ + const HasNoPageLink = acf.Condition.extend( { type: 'hasNoPageLink', operator: '==empty', - label: __('Has no page selected'), - fieldTypes: ['page_link'], - match: function (rule, field) { + label: __( 'Has no page selected' ), + fieldTypes: [ 'page_link' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !val; + return ! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasNoPageLink); + acf.registerConditionType( HasNoPageLink ); /** * Adds condition for user field having user equal to. * * @since ACF 6.3 */ - const HasUser = acf.Condition.extend({ + const HasUser = acf.Condition.extend( { type: 'hasUser', operator: '==', - label: __('User is equal to'), - fieldTypes: ['user'], - match: function (rule, field) { - return isEqualToNumber(rule.value, field.val()); + label: __( 'User is equal to' ), + fieldTypes: [ 'user' ], + match: function ( rule, field ) { + return isEqualToNumber( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'user'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'user' ); }, - }); + } ); - acf.registerConditionType(HasUser); + acf.registerConditionType( HasUser ); /** * Adds condition for user field having user not equal to. * * @since ACF 6.3 */ - const HasUserNotEqual = acf.Condition.extend({ + const HasUserNotEqual = acf.Condition.extend( { type: 'hasUserNotEqual', operator: '!==', - label: __('User is not equal to'), - fieldTypes: ['user'], - match: function (rule, field) { - return !isEqualToNumber(rule.value, field.val()); + label: __( 'User is not equal to' ), + fieldTypes: [ 'user' ], + match: function ( rule, field ) { + return ! isEqualToNumber( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'user'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'user' ); }, - }); + } ); - acf.registerConditionType(HasUserNotEqual); + acf.registerConditionType( HasUserNotEqual ); /** * Adds condition for user field containing a specific user. * * @since ACF 6.3 */ - const containsUser = acf.Condition.extend({ + const containsUser = acf.Condition.extend( { type: 'containsUser', operator: '==contains', - label: __('Users contain'), - fieldTypes: ['user'], - match: function (rule, field) { + label: __( 'Users contain' ), + fieldTypes: [ 'user' ], + match: function ( rule, field ) { const val = field.val(); const ruleVal = rule.value; let match = false; - if (val instanceof Array) { - match = val.includes(ruleVal); + if ( val instanceof Array ) { + match = val.includes( ruleVal ); } else { match = val === ruleVal; } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'user'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'user' ); }, - }); + } ); - acf.registerConditionType(containsUser); + acf.registerConditionType( containsUser ); /** * Adds condition for user field not containing a specific user. * * @since ACF 6.3 */ - const containsNotUser = acf.Condition.extend({ + const containsNotUser = acf.Condition.extend( { type: 'containsNotUser', operator: '!=contains', - label: __('Users do not contain'), - fieldTypes: ['user'], - match: function (rule, field) { + label: __( 'Users do not contain' ), + fieldTypes: [ 'user' ], + match: function ( rule, field ) { const val = field.val(); const ruleVal = rule.value; let match = true; - if (val instanceof Array) { - match = !val.includes(ruleVal); + if ( val instanceof Array ) { + match = ! val.includes( ruleVal ); } else { - match = !val === ruleVal; + match = ! val === ruleVal; } }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'user'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'user' ); }, - }); + } ); - acf.registerConditionType(containsNotUser); + acf.registerConditionType( containsNotUser ); /** * Adds condition for when any user is selected. * * @since ACF 6.3 */ - const HasAnyUser = acf.Condition.extend({ + const HasAnyUser = acf.Condition.extend( { type: 'hasAnyUser', operator: '!=empty', - label: __('Has any user selected'), - fieldTypes: ['user'], - match: function (rule, field) { + label: __( 'Has any user selected' ), + fieldTypes: [ 'user' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !!val; + return !! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasAnyUser); + acf.registerConditionType( HasAnyUser ); /** * Adds condition for when no user is selected. * * @since ACF 6.3 */ - const HasNoUser = acf.Condition.extend({ + const HasNoUser = acf.Condition.extend( { type: 'hasNoUser', operator: '==empty', - label: __('Has no user selected'), - fieldTypes: ['user'], - match: function (rule, field) { + label: __( 'Has no user selected' ), + fieldTypes: [ 'user' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !val; + return ! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasNoUser); + acf.registerConditionType( HasNoUser ); /** * Adds condition for Relationship having Relationship equal to. * * @since ACF 6.3 */ - const HasRelationship = acf.Condition.extend({ + const HasRelationship = acf.Condition.extend( { type: 'hasRelationship', operator: '==', - label: __('Relationship is equal to'), - fieldTypes: ['relationship'], - match: function (rule, field) { - return isEqualToNumber(rule.value, field.val()); + label: __( 'Relationship is equal to' ), + fieldTypes: [ 'relationship' ], + match: function ( rule, field ) { + return isEqualToNumber( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'relationship'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'relationship' ); }, - }); + } ); - acf.registerConditionType(HasRelationship); + acf.registerConditionType( HasRelationship ); /** * Adds condition for selection having Relationship not equal to. * * @since ACF 6.3 */ - const HasRelationshipNotEqual = acf.Condition.extend({ + const HasRelationshipNotEqual = acf.Condition.extend( { type: 'hasRelationshipNotEqual', operator: '!==', - label: __('Relationship is not equal to'), - fieldTypes: ['relationship'], - match: function (rule, field) { - return !isEqualToNumber(rule.value, field.val()); + label: __( 'Relationship is not equal to' ), + fieldTypes: [ 'relationship' ], + match: function ( rule, field ) { + return ! isEqualToNumber( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'relationship'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'relationship' ); }, - }); + } ); - acf.registerConditionType(HasRelationshipNotEqual); + acf.registerConditionType( HasRelationshipNotEqual ); /** * Adds condition for Relationship containing a specific Relationship. * * @since ACF 6.3 */ - const containsRelationship = acf.Condition.extend({ + const containsRelationship = acf.Condition.extend( { type: 'containsRelationship', operator: '==contains', - label: __('Relationships contain'), - fieldTypes: ['relationship'], - match: function (rule, field) { + label: __( 'Relationships contain' ), + fieldTypes: [ 'relationship' ], + match: function ( rule, field ) { const val = field.val(); // Relationships are stored as strings, use float to compare to field's rule value. - const ruleVal = parseInt(rule.value); + const ruleVal = parseInt( rule.value ); let match = false; - if (val instanceof Array) { - match = val.includes(ruleVal); + if ( val instanceof Array ) { + match = val.includes( ruleVal ); } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'relationship'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'relationship' ); }, - }); + } ); - acf.registerConditionType(containsRelationship); + acf.registerConditionType( containsRelationship ); /** * Adds condition for Relationship not containing a specific Relationship. * * @since ACF 6.3 */ - const containsNotRelationship = acf.Condition.extend({ + const containsNotRelationship = acf.Condition.extend( { type: 'containsNotRelationship', operator: '!=contains', - label: __('Relationships do not contain'), - fieldTypes: ['relationship'], - match: function (rule, field) { + label: __( 'Relationships do not contain' ), + fieldTypes: [ 'relationship' ], + match: function ( rule, field ) { const val = field.val(); // Relationships are stored as strings, use float to compare to field's rule value. - const ruleVal = parseInt(rule.value); + const ruleVal = parseInt( rule.value ); let match = true; - if (val instanceof Array) { - match = !val.includes(ruleVal); + if ( val instanceof Array ) { + match = ! val.includes( ruleVal ); } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'relationship'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'relationship' ); }, - }); + } ); - acf.registerConditionType(containsNotRelationship); + acf.registerConditionType( containsNotRelationship ); /** * Adds condition for when any relation is selected. * * @since ACF 6.3 */ - const HasAnyRelation = acf.Condition.extend({ + const HasAnyRelation = acf.Condition.extend( { type: 'hasAnyRelation', operator: '!=empty', - label: __('Has any relationship selected'), - fieldTypes: ['relationship'], - match: function (rule, field) { + label: __( 'Has any relationship selected' ), + fieldTypes: [ 'relationship' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !!val; + return !! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasAnyRelation); + acf.registerConditionType( HasAnyRelation ); /** * Adds condition for when no relation is selected. * * @since ACF 6.3 */ - const HasNoRelation = acf.Condition.extend({ + const HasNoRelation = acf.Condition.extend( { type: 'hasNoRelation', operator: '==empty', - label: __('Has no relationship selected'), - fieldTypes: ['relationship'], - match: function (rule, field) { + label: __( 'Has no relationship selected' ), + fieldTypes: [ 'relationship' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !val; + return ! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasNoRelation); + acf.registerConditionType( HasNoRelation ); /** * Adds condition for having post equal to. * * @since ACF 6.3 */ - const HasPostObject = acf.Condition.extend({ + const HasPostObject = acf.Condition.extend( { type: 'hasPostObject', operator: '==', - label: __('Post is equal to'), - fieldTypes: ['post_object'], - match: function (rule, field) { - return isEqualToNumber(rule.value, field.val()); + label: __( 'Post is equal to' ), + fieldTypes: [ 'post_object' ], + match: function ( rule, field ) { + return isEqualToNumber( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'post_object'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'post_object' ); }, - }); + } ); - acf.registerConditionType(HasPostObject); + acf.registerConditionType( HasPostObject ); /** * Adds condition for selection having post not equal to. * * @since ACF 6.3 */ - const HasPostObjectNotEqual = acf.Condition.extend({ + const HasPostObjectNotEqual = acf.Condition.extend( { type: 'hasPostObjectNotEqual', operator: '!==', - label: __('Post is not equal to'), - fieldTypes: ['post_object'], - match: function (rule, field) { - return !isEqualToNumber(rule.value, field.val()); + label: __( 'Post is not equal to' ), + fieldTypes: [ 'post_object' ], + match: function ( rule, field ) { + return ! isEqualToNumber( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'post_object'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'post_object' ); }, - }); + } ); - acf.registerConditionType(HasPostObjectNotEqual); + acf.registerConditionType( HasPostObjectNotEqual ); /** * Adds condition for Relationship containing a specific Relationship. * * @since ACF 6.3 */ - const containsPostObject = acf.Condition.extend({ + const containsPostObject = acf.Condition.extend( { type: 'containsPostObject', operator: '==contains', - label: __('Posts contain'), - fieldTypes: ['post_object'], - match: function (rule, field) { + label: __( 'Posts contain' ), + fieldTypes: [ 'post_object' ], + match: function ( rule, field ) { const val = field.val(); const ruleVal = rule.value; let match = false; - if (val instanceof Array) { - match = val.includes(ruleVal); + if ( val instanceof Array ) { + match = val.includes( ruleVal ); } else { match = val === ruleVal; } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'post_object'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'post_object' ); }, - }); + } ); - acf.registerConditionType(containsPostObject); + acf.registerConditionType( containsPostObject ); /** * Adds condition for Relationship not containing a specific Relationship. * * @since ACF 6.3 */ - const containsNotPostObject = acf.Condition.extend({ + const containsNotPostObject = acf.Condition.extend( { type: 'containsNotPostObject', operator: '!=contains', - label: __('Posts do not contain'), - fieldTypes: ['post_object'], - match: function (rule, field) { + label: __( 'Posts do not contain' ), + fieldTypes: [ 'post_object' ], + match: function ( rule, field ) { const val = field.val(); const ruleVal = rule.value; let match = true; - if (val instanceof Array) { - match = !val.includes(ruleVal); + if ( val instanceof Array ) { + match = ! val.includes( ruleVal ); } else { match = val !== ruleVal; } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'post_object'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'post_object' ); }, - }); + } ); - acf.registerConditionType(containsNotPostObject); + acf.registerConditionType( containsNotPostObject ); /** * Adds condition for when any post is selected. * * @since ACF 6.3 */ - const HasAnyPostObject = acf.Condition.extend({ + const HasAnyPostObject = acf.Condition.extend( { type: 'hasAnyPostObject', operator: '!=empty', - label: __('Has any post selected'), - fieldTypes: ['post_object'], - match: function (rule, field) { + label: __( 'Has any post selected' ), + fieldTypes: [ 'post_object' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !!val; + return !! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasAnyPostObject); + acf.registerConditionType( HasAnyPostObject ); /** * Adds condition for when no post is selected. * * @since ACF 6.3 */ - const HasNoPostObject = acf.Condition.extend({ + const HasNoPostObject = acf.Condition.extend( { type: 'hasNoPostObject', operator: '==empty', - label: __('Has no post selected'), - fieldTypes: ['post_object'], - match: function (rule, field) { + label: __( 'Has no post selected' ), + fieldTypes: [ 'post_object' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !val; + return ! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasNoPostObject); + acf.registerConditionType( HasNoPostObject ); /** * Adds condition for taxonomy having term equal to. * * @since ACF 6.3 */ - const HasTerm = acf.Condition.extend({ + const HasTerm = acf.Condition.extend( { type: 'hasTerm', operator: '==', - label: __('Term is equal to'), - fieldTypes: ['taxonomy'], - match: function (rule, field) { - return isEqualToNumber(rule.value, field.val()); + label: __( 'Term is equal to' ), + fieldTypes: [ 'taxonomy' ], + match: function ( rule, field ) { + return isEqualToNumber( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'taxonomy'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'taxonomy' ); }, - }); + } ); - acf.registerConditionType(HasTerm); + acf.registerConditionType( HasTerm ); /** * Adds condition for taxonomy having term not equal to. * * @since ACF 6.3 */ - const hasTermNotEqual = acf.Condition.extend({ + const hasTermNotEqual = acf.Condition.extend( { type: 'hasTermNotEqual', operator: '!==', - label: __('Term is not equal to'), - fieldTypes: ['taxonomy'], - match: function (rule, field) { - return !isEqualToNumber(rule.value, field.val()); + label: __( 'Term is not equal to' ), + fieldTypes: [ 'taxonomy' ], + match: function ( rule, field ) { + return ! isEqualToNumber( rule.value, field.val() ); }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'taxonomy'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'taxonomy' ); }, - }); + } ); - acf.registerConditionType(hasTermNotEqual); + acf.registerConditionType( hasTermNotEqual ); /** * Adds condition for taxonomy containing a specific term. * * @since ACF 6.3 */ - const containsTerm = acf.Condition.extend({ + const containsTerm = acf.Condition.extend( { type: 'containsTerm', operator: '==contains', - label: __('Terms contain'), - fieldTypes: ['taxonomy'], - match: function (rule, field) { + label: __( 'Terms contain' ), + fieldTypes: [ 'taxonomy' ], + match: function ( rule, field ) { const val = field.val(); const ruleVal = rule.value; let match = false; - if (val instanceof Array) { - match = val.includes(ruleVal); + if ( val instanceof Array ) { + match = val.includes( ruleVal ); } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'taxonomy'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'taxonomy' ); }, - }); + } ); - acf.registerConditionType(containsTerm); + acf.registerConditionType( containsTerm ); /** * Adds condition for taxonomy not containing a specific term. * * @since ACF 6.3 */ - const containsNotTerm = acf.Condition.extend({ + const containsNotTerm = acf.Condition.extend( { type: 'containsNotTerm', operator: '!=contains', - label: __('Terms do not contain'), - fieldTypes: ['taxonomy'], - match: function (rule, field) { + label: __( 'Terms do not contain' ), + fieldTypes: [ 'taxonomy' ], + match: function ( rule, field ) { const val = field.val(); const ruleVal = rule.value; let match = true; - if (val instanceof Array) { - match = !val.includes(ruleVal); + if ( val instanceof Array ) { + match = ! val.includes( ruleVal ); } return match; }, - choices: function (fieldObject) { - return conditionalSelect2(fieldObject, 'taxonomy'); + choices: function ( fieldObject ) { + return conditionalSelect2( fieldObject, 'taxonomy' ); }, - }); + } ); - acf.registerConditionType(containsNotTerm); + acf.registerConditionType( containsNotTerm ); /** * Adds condition for when any term is selected. * * @since ACF 6.3 */ - const HasAnyTerm = acf.Condition.extend({ + const HasAnyTerm = acf.Condition.extend( { type: 'hasAnyTerm', operator: '!=empty', - label: __('Has any term selected'), - fieldTypes: ['taxonomy'], - match: function (rule, field) { + label: __( 'Has any term selected' ), + fieldTypes: [ 'taxonomy' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !!val; + return !! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasAnyTerm); + acf.registerConditionType( HasAnyTerm ); /** * Adds condition for when no term is selected. * * @since ACF 6.3 */ - const HasNoTerm = acf.Condition.extend({ + const HasNoTerm = acf.Condition.extend( { type: 'hasNoTerm', operator: '==empty', - label: __('Has no term selected'), - fieldTypes: ['taxonomy'], - match: function (rule, field) { + label: __( 'Has no term selected' ), + fieldTypes: [ 'taxonomy' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return !val; + return ! val; }, choices: function () { return ''; }, - }); + } ); - acf.registerConditionType(HasNoTerm); + acf.registerConditionType( HasNoTerm ); /** * hasValue @@ -841,10 +845,10 @@ * @param void * @return void */ - const HasValue = acf.Condition.extend({ + const HasValue = acf.Condition.extend( { type: 'hasValue', operator: '!=empty', - label: __('Has any value'), + label: __( 'Has any value' ), fieldTypes: [ 'text', 'textarea', @@ -869,19 +873,19 @@ 'color_picker', 'icon_picker', ], - match: function (rule, field) { + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } return val ? true : false; }, - choices: function (fieldObject) { + choices: function ( fieldObject ) { return ''; }, - }); + } ); - acf.registerConditionType(HasValue); + acf.registerConditionType( HasValue ); /** * hasValue @@ -892,16 +896,16 @@ * @param void * @return void */ - const HasNoValue = HasValue.extend({ + const HasNoValue = HasValue.extend( { type: 'hasNoValue', operator: '==empty', - label: __('Has no value'), - match: function (rule, field) { - return !HasValue.prototype.match.apply(this, arguments); + label: __( 'Has no value' ), + match: function ( rule, field ) { + return ! HasValue.prototype.match.apply( this, arguments ); }, - }); + } ); - acf.registerConditionType(HasNoValue); + acf.registerConditionType( HasNoValue ); /** * EqualTo @@ -912,10 +916,10 @@ * @param void * @return void */ - const EqualTo = acf.Condition.extend({ + const EqualTo = acf.Condition.extend( { type: 'equalTo', operator: '==', - label: __('Value is equal to'), + label: __( 'Value is equal to' ), fieldTypes: [ 'text', 'textarea', @@ -925,19 +929,19 @@ 'url', 'password', ], - match: function (rule, field) { - if (acf.isNumeric(rule.value)) { - return isEqualToNumber(rule.value, field.val()); + match: function ( rule, field ) { + if ( acf.isNumeric( rule.value ) ) { + return isEqualToNumber( rule.value, field.val() ); } else { - return isEqualTo(rule.value, field.val()); + return isEqualTo( rule.value, field.val() ); } }, - choices: function (fieldObject) { + choices: function ( fieldObject ) { return ''; }, - }); + } ); - acf.registerConditionType(EqualTo); + acf.registerConditionType( EqualTo ); /** * NotEqualTo @@ -948,16 +952,16 @@ * @param void * @return void */ - const NotEqualTo = EqualTo.extend({ + const NotEqualTo = EqualTo.extend( { type: 'notEqualTo', operator: '!=', - label: __('Value is not equal to'), - match: function (rule, field) { - return !EqualTo.prototype.match.apply(this, arguments); + label: __( 'Value is not equal to' ), + match: function ( rule, field ) { + return ! EqualTo.prototype.match.apply( this, arguments ); }, - }); + } ); - acf.registerConditionType(NotEqualTo); + acf.registerConditionType( NotEqualTo ); /** * PatternMatch @@ -968,20 +972,27 @@ * @param void * @return void */ - const PatternMatch = acf.Condition.extend({ + const PatternMatch = acf.Condition.extend( { type: 'patternMatch', operator: '==pattern', - label: __('Value matches pattern'), - fieldTypes: ['text', 'textarea', 'email', 'url', 'password', 'wysiwyg'], - match: function (rule, field) { - return matchesPattern(field.val(), rule.value); + label: __( 'Value matches pattern' ), + fieldTypes: [ + 'text', + 'textarea', + 'email', + 'url', + 'password', + 'wysiwyg', + ], + match: function ( rule, field ) { + return matchesPattern( field.val(), rule.value ); }, - choices: function (fieldObject) { + choices: function ( fieldObject ) { return ''; }, - }); + } ); - acf.registerConditionType(PatternMatch); + acf.registerConditionType( PatternMatch ); /** * Contains @@ -992,10 +1003,10 @@ * @param void * @return void */ - const Contains = acf.Condition.extend({ + const Contains = acf.Condition.extend( { type: 'contains', operator: '==contains', - label: __('Value contains'), + label: __( 'Value contains' ), fieldTypes: [ 'text', 'textarea', @@ -1007,15 +1018,15 @@ 'oembed', 'select', ], - match: function (rule, field) { - return containsString(field.val(), rule.value); + match: function ( rule, field ) { + return containsString( field.val(), rule.value ); }, - choices: function (fieldObject) { + choices: function ( fieldObject ) { return ''; }, - }); + } ); - acf.registerConditionType(Contains); + acf.registerConditionType( Contains ); /** * TrueFalseEqualTo @@ -1026,21 +1037,21 @@ * @param void * @return void */ - const TrueFalseEqualTo = EqualTo.extend({ + const TrueFalseEqualTo = EqualTo.extend( { type: 'trueFalseEqualTo', choiceType: 'select', - fieldTypes: ['true_false'], - choices: function (field) { + fieldTypes: [ 'true_false' ], + choices: function ( field ) { return [ { id: 1, - text: __('Checked'), + text: __( 'Checked' ), }, ]; }, - }); + } ); - acf.registerConditionType(TrueFalseEqualTo); + acf.registerConditionType( TrueFalseEqualTo ); /** * TrueFalseNotEqualTo @@ -1051,21 +1062,21 @@ * @param void * @return void */ - const TrueFalseNotEqualTo = NotEqualTo.extend({ + const TrueFalseNotEqualTo = NotEqualTo.extend( { type: 'trueFalseNotEqualTo', choiceType: 'select', - fieldTypes: ['true_false'], - choices: function (field) { + fieldTypes: [ 'true_false' ], + choices: function ( field ) { return [ { id: 1, - text: __('Checked'), + text: __( 'Checked' ), }, ]; }, - }); + } ); - acf.registerConditionType(TrueFalseNotEqualTo); + acf.registerConditionType( TrueFalseNotEqualTo ); /** * SelectEqualTo @@ -1076,56 +1087,56 @@ * @param void * @return void */ - const SelectEqualTo = acf.Condition.extend({ + const SelectEqualTo = acf.Condition.extend( { type: 'selectEqualTo', operator: '==', - label: __('Value is equal to'), - fieldTypes: ['select', 'checkbox', 'radio', 'button_group'], - match: function (rule, field) { + label: __( 'Value is equal to' ), + fieldTypes: [ 'select', 'checkbox', 'radio', 'button_group' ], + match: function ( rule, field ) { const val = field.val(); - if (val instanceof Array) { - return inArray(rule.value, val); + if ( val instanceof Array ) { + return inArray( rule.value, val ); } else { - return isEqualTo(rule.value, val); + return isEqualTo( rule.value, val ); } }, - choices: function (fieldObject) { + choices: function ( fieldObject ) { // vars const choices = []; const lines = fieldObject - .$setting('choices textarea') + .$setting( 'choices textarea' ) .val() - .split('\n'); + .split( '\n' ); // allow null - if (fieldObject.$input('allow_null').prop('checked')) { - choices.push({ + if ( fieldObject.$input( 'allow_null' ).prop( 'checked' ) ) { + choices.push( { id: '', - text: __('Null'), - }); + text: __( 'Null' ), + } ); } // loop - lines.map(function (line) { + lines.map( function ( line ) { // split - line = line.split(':'); + line = line.split( ':' ); // default label to value - line[1] = line[1] || line[0]; + line[ 1 ] = line[ 1 ] || line[ 0 ]; // append - choices.push({ - id: line[0].trim(), - text: line[1].trim(), - }); - }); + choices.push( { + id: line[ 0 ].trim(), + text: line[ 1 ].trim(), + } ); + } ); // return return choices; }, - }); + } ); - acf.registerConditionType(SelectEqualTo); + acf.registerConditionType( SelectEqualTo ); /** * SelectNotEqualTo @@ -1136,16 +1147,16 @@ * @param void * @return void */ - const SelectNotEqualTo = SelectEqualTo.extend({ + const SelectNotEqualTo = SelectEqualTo.extend( { type: 'selectNotEqualTo', operator: '!=', - label: __('Value is not equal to'), - match: function (rule, field) { - return !SelectEqualTo.prototype.match.apply(this, arguments); + label: __( 'Value is not equal to' ), + match: function ( rule, field ) { + return ! SelectEqualTo.prototype.match.apply( this, arguments ); }, - }); + } ); - acf.registerConditionType(SelectNotEqualTo); + acf.registerConditionType( SelectNotEqualTo ); /** * GreaterThan @@ -1156,24 +1167,24 @@ * @param void * @return void */ - const GreaterThan = acf.Condition.extend({ + const GreaterThan = acf.Condition.extend( { type: 'greaterThan', operator: '>', - label: __('Value is greater than'), - fieldTypes: ['number', 'range'], - match: function (rule, field) { + label: __( 'Value is greater than' ), + fieldTypes: [ 'number', 'range' ], + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - return isGreaterThan(val, rule.value); + return isGreaterThan( val, rule.value ); }, - choices: function (fieldObject) { + choices: function ( fieldObject ) { return ''; }, - }); + } ); - acf.registerConditionType(GreaterThan); + acf.registerConditionType( GreaterThan ); /** * LessThan @@ -1184,26 +1195,26 @@ * @param void * @return void */ - const LessThan = GreaterThan.extend({ + const LessThan = GreaterThan.extend( { type: 'lessThan', operator: '<', - label: __('Value is less than'), - match: function (rule, field) { + label: __( 'Value is less than' ), + match: function ( rule, field ) { let val = field.val(); - if (val instanceof Array) { + if ( val instanceof Array ) { val = val.length; } - if (val === undefined || val === null || val === false) { + if ( val === undefined || val === null || val === false ) { return true; } - return isLessThan(val, rule.value); + return isLessThan( val, rule.value ); }, - choices: function (fieldObject) { + choices: function ( fieldObject ) { return ''; }, - }); + } ); - acf.registerConditionType(LessThan); + acf.registerConditionType( LessThan ); /** * SelectedGreaterThan @@ -1214,9 +1225,9 @@ * @param void * @return void */ - const SelectionGreaterThan = GreaterThan.extend({ + const SelectionGreaterThan = GreaterThan.extend( { type: 'selectionGreaterThan', - label: __('Selection is greater than'), + label: __( 'Selection is greater than' ), fieldTypes: [ 'checkbox', 'select', @@ -1226,9 +1237,9 @@ 'taxonomy', 'user', ], - }); + } ); - acf.registerConditionType(SelectionGreaterThan); + acf.registerConditionType( SelectionGreaterThan ); /** * SelectionLessThan @@ -1239,9 +1250,9 @@ * @param void * @return void */ - const SelectionLessThan = LessThan.extend({ + const SelectionLessThan = LessThan.extend( { type: 'selectionLessThan', - label: __('Selection is less than'), + label: __( 'Selection is less than' ), fieldTypes: [ 'checkbox', 'select', @@ -1251,7 +1262,7 @@ 'taxonomy', 'user', ], - }); + } ); - acf.registerConditionType(SelectionLessThan); -})(jQuery); + acf.registerConditionType( SelectionLessThan ); +} )( jQuery ); diff --git a/assets/src/js/_acf-field-button-group.js b/assets/src/js/_acf-field-button-group.js index fb39e103..5a6a07ef 100644 --- a/assets/src/js/_acf-field-button-group.js +++ b/assets/src/js/_acf-field-button-group.js @@ -1,7 +1,7 @@ import { update } from '@wordpress/icons'; -(function ($, undefined) { - const Field = acf.Field.extend({ +( function ( $, undefined ) { + const Field = acf.Field.extend( { type: 'button_group', events: { @@ -10,63 +10,63 @@ import { update } from '@wordpress/icons'; }, $control: function () { - return this.$('.acf-button-group'); + return this.$( '.acf-button-group' ); }, $input: function () { - return this.$('input:checked'); + return this.$( 'input:checked' ); }, initialize: function () { this.updateButtonStates(); }, - setValue: function (val) { - this.$('input[value="' + val + '"]') - .prop('checked', true) - .trigger('change'); + setValue: function ( val ) { + this.$( 'input[value="' + val + '"]' ) + .prop( 'checked', true ) + .trigger( 'change' ); this.updateButtonStates(); }, updateButtonStates: function () { - const labels = this.$control().find('label'); + const labels = this.$control().find( 'label' ); const input = this.$input(); labels - .removeClass('selected') - .attr('aria-checked', 'false') - .attr('tabindex', '-1'); - if (input.length) { + .removeClass( 'selected' ) + .attr( 'aria-checked', 'false' ) + .attr( 'tabindex', '-1' ); + if ( input.length ) { // If there's a checked input, mark its parent label as selected input - .parent('label') - .addClass('selected') - .attr('aria-checked', 'true') - .attr('tabindex', '0'); + .parent( 'label' ) + .addClass( 'selected' ) + .attr( 'aria-checked', 'true' ) + .attr( 'tabindex', '0' ); } else { - labels.first().attr('tabindex', '0'); + labels.first().attr( 'tabindex', '0' ); } }, - onClick: function (e, $el) { - this.selectButton($el.parent('label')); + onClick: function ( e, $el ) { + this.selectButton( $el.parent( 'label' ) ); }, - onKeyDown: function (event, label) { + onKeyDown: function ( event, label ) { const key = event.which; // Space or Enter: select the button - if (key === 13 || key === 32) { + if ( key === 13 || key === 32 ) { event.preventDefault(); - this.selectButton(label); + this.selectButton( label ); return; } // Arrow keys: move focus between buttons - if (key === 37 || key === 39 || key === 38 || key === 40) { + if ( key === 37 || key === 39 || key === 38 || key === 40 ) { event.preventDefault(); - const labels = this.$control().find('label'); - const currentIndex = labels.index(label); + const labels = this.$control().find( 'label' ); + const currentIndex = labels.index( label ); let nextIndex; // Left/Up arrow: move to previous, wrap to last if at start - if (key === 37 || key === 38) { + if ( key === 37 || key === 38 ) { nextIndex = currentIndex > 0 ? currentIndex - 1 : labels.length - 1; } @@ -76,22 +76,22 @@ import { update } from '@wordpress/icons'; currentIndex < labels.length - 1 ? currentIndex + 1 : 0; } - const nextLabel = labels.eq(nextIndex); - labels.attr('tabindex', '-1'); - nextLabel.attr('tabindex', '0').trigger('focus'); + const nextLabel = labels.eq( nextIndex ); + labels.attr( 'tabindex', '-1' ); + nextLabel.attr( 'tabindex', '0' ).trigger( 'focus' ); } }, - selectButton: function (element) { - const inputRadio = element.find('input[type="radio"]'); - const isSelected = element.hasClass('selected'); - inputRadio.prop('checked', true).trigger('change'); - if (this.get('allow_null') && isSelected) { - inputRadio.prop('checked', false).trigger('change'); + selectButton: function ( element ) { + const inputRadio = element.find( 'input[type="radio"]' ); + const isSelected = element.hasClass( 'selected' ); + inputRadio.prop( 'checked', true ).trigger( 'change' ); + if ( this.get( 'allow_null' ) && isSelected ) { + inputRadio.prop( 'checked', false ).trigger( 'change' ); } this.updateButtonStates(); }, - }); + } ); - acf.registerFieldType(Field); -})(jQuery); + acf.registerFieldType( Field ); +} )( jQuery ); diff --git a/assets/src/js/_acf-field-color-picker.js b/assets/src/js/_acf-field-color-picker.js index 72bec2ce..46807526 100644 --- a/assets/src/js/_acf-field-color-picker.js +++ b/assets/src/js/_acf-field-color-picker.js @@ -1,5 +1,5 @@ -(function ($, undefined) { - var Field = acf.Field.extend({ +( function ( $, undefined ) { + var Field = acf.Field.extend( { type: 'color_picker', wait: 'load', @@ -9,23 +9,23 @@ }, $control: function () { - return this.$('.acf-color-picker'); + return this.$( '.acf-color-picker' ); }, $input: function () { - return this.$('input[type="hidden"]'); + return this.$( 'input[type="hidden"]' ); }, $inputText: function () { - return this.$('input[type="text"]'); + return this.$( 'input[type="text"]' ); }, - setValue: function (val) { + setValue: function ( val ) { // update input (with change) - acf.val(this.$input(), val); + acf.val( this.$input(), val ); // update iris - this.$inputText().iris('color', val); + this.$inputText().iris( 'color', val ); }, initialize: function () { @@ -34,11 +34,11 @@ var $inputText = this.$inputText(); // event - var onChange = function (e) { + var onChange = function ( e ) { // timeout is required to ensure the $input val is correct - setTimeout(function () { - acf.val($input, $inputText.val()); - }, 1); + setTimeout( function () { + acf.val( $input, $inputText.val() ); + }, 1 ); }; // args @@ -49,33 +49,33 @@ change: onChange, clear: onChange, }; - if ('custom' === $inputText.data('acf-palette-type')) { + if ( 'custom' === $inputText.data( 'acf-palette-type' ) ) { const paletteColor = $inputText - .data('acf-palette-colors') + .data( 'acf-palette-colors' ) .match( /#(?:[0-9a-fA-F]{3}){1,2}|rgba?\([\s*(\d|.)+\s*,]+\)/g ); - if (paletteColor) { - let trimmed = paletteColor.map((color) => color.trim()); + if ( paletteColor ) { + let trimmed = paletteColor.map( ( color ) => color.trim() ); args.palettes = trimmed; } } // filter - var args = acf.applyFilters('color_picker_args', args, this); + var args = acf.applyFilters( 'color_picker_args', args, this ); // initialize - $inputText.wpColorPicker(args); + $inputText.wpColorPicker( args ); }, - onDuplicate: function (e, $el, $duplicate) { + onDuplicate: function ( e, $el, $duplicate ) { // The wpColorPicker library does not provide a destroy method. // Manually reset DOM by replacing elements back to their original state. - $colorPicker = $duplicate.find('.wp-picker-container'); - $inputText = $duplicate.find('input[type="text"]'); - $colorPicker.replaceWith($inputText); + $colorPicker = $duplicate.find( '.wp-picker-container' ); + $inputText = $duplicate.find( 'input[type="text"]' ); + $colorPicker.replaceWith( $inputText ); }, - }); + } ); - acf.registerFieldType(Field); -})(jQuery); + acf.registerFieldType( Field ); +} )( jQuery ); diff --git a/assets/src/js/_acf-field-radio.js b/assets/src/js/_acf-field-radio.js index 1fb9bdc8..7d37078b 100644 --- a/assets/src/js/_acf-field-radio.js +++ b/assets/src/js/_acf-field-radio.js @@ -1,5 +1,5 @@ -(function ($, undefined) { - var Field = acf.Field.extend({ +( function ( $, undefined ) { + var Field = acf.Field.extend( { type: 'radio', events: { @@ -8,63 +8,63 @@ }, $control: function () { - return this.$('.acf-radio-list'); + return this.$( '.acf-radio-list' ); }, $input: function () { - return this.$('input:checked'); + return this.$( 'input:checked' ); }, $inputText: function () { - return this.$('input[type="text"]'); + return this.$( 'input[type="text"]' ); }, getValue: function () { var val = this.$input().val(); - if (val === 'other' && this.get('other_choice')) { + if ( val === 'other' && this.get( 'other_choice' ) ) { val = this.$inputText().val(); } return val; }, - onClick: function (e, $el) { + onClick: function ( e, $el ) { // vars - var $label = $el.parent('label'); - var selected = $label.hasClass('selected'); + var $label = $el.parent( 'label' ); + var selected = $label.hasClass( 'selected' ); var val = $el.val(); // remove previous selected - this.$('.selected').removeClass('selected'); + this.$( '.selected' ).removeClass( 'selected' ); // add active class - $label.addClass('selected'); + $label.addClass( 'selected' ); // allow null - if (this.get('allow_null') && selected) { - $label.removeClass('selected'); - $el.prop('checked', false).trigger('change'); + if ( this.get( 'allow_null' ) && selected ) { + $label.removeClass( 'selected' ); + $el.prop( 'checked', false ).trigger( 'change' ); val = false; } // other - if (this.get('other_choice')) { + if ( this.get( 'other_choice' ) ) { // enable - if (val === 'other') { - this.$inputText().prop('disabled', false); + if ( val === 'other' ) { + this.$inputText().prop( 'disabled', false ); // disable } else { - this.$inputText().prop('disabled', true); + this.$inputText().prop( 'disabled', true ); } } }, - onKeyDownInput: function (event, input) { - if (event.which === 13) { + onKeyDownInput: function ( event, input ) { + if ( event.which === 13 ) { event.preventDefault(); - input.prop('checked', true).trigger('change'); + input.prop( 'checked', true ).trigger( 'change' ); } }, - }); + } ); - acf.registerFieldType(Field); -})(jQuery); + acf.registerFieldType( Field ); +} )( jQuery ); diff --git a/assets/src/js/_acf-field-taxonomy.js b/assets/src/js/_acf-field-taxonomy.js index 9fe651d1..2b0915c7 100644 --- a/assets/src/js/_acf-field-taxonomy.js +++ b/assets/src/js/_acf-field-taxonomy.js @@ -1,5 +1,5 @@ -(function ($, undefined) { - var Field = acf.Field.extend({ +( function ( $, undefined ) { + var Field = acf.Field.extend( { type: 'taxonomy', data: { @@ -18,19 +18,19 @@ }, $control: function () { - return this.$('.acf-taxonomy-field'); + return this.$( '.acf-taxonomy-field' ); }, $input: function () { - return this.getRelatedPrototype().$input.apply(this, arguments); + return this.getRelatedPrototype().$input.apply( this, arguments ); }, getRelatedType: function () { // vars - var fieldType = this.get('ftype'); + var fieldType = this.get( 'ftype' ); // normalize - if (fieldType == 'multi_select') { + if ( fieldType == 'multi_select' ) { fieldType = 'select'; } @@ -39,29 +39,29 @@ }, getRelatedPrototype: function () { - return acf.getFieldType(this.getRelatedType()).prototype; + return acf.getFieldType( this.getRelatedType() ).prototype; }, getValue: function () { - return this.getRelatedPrototype().getValue.apply(this, arguments); + return this.getRelatedPrototype().getValue.apply( this, arguments ); }, setValue: function () { - return this.getRelatedPrototype().setValue.apply(this, arguments); + return this.getRelatedPrototype().setValue.apply( this, arguments ); }, initialize: function () { - this.getRelatedPrototype().initialize.apply(this, arguments); + this.getRelatedPrototype().initialize.apply( this, arguments ); }, onRemove: function () { var proto = this.getRelatedPrototype(); - if (proto.onRemove) { - proto.onRemove.apply(this, arguments); + if ( proto.onRemove ) { + proto.onRemove.apply( this, arguments ); } }, - onClickAdd: function (e, $el) { + onClickAdd: function ( e, $el ) { // vars var field = this; var popup = false; @@ -75,124 +75,124 @@ // step 1. var step1 = function () { // popup - popup = acf.newPopup({ - title: $el.attr('title'), + popup = acf.newPopup( { + title: $el.attr( 'title' ), loading: true, width: '300px', - }); + } ); // ajax var ajaxData = { action: 'acf/fields/taxonomy/add_term', - field_key: field.get('key'), - nonce: field.get('nonce'), + field_key: field.get( 'key' ), + nonce: field.get( 'nonce' ), }; // get HTML - $.ajax({ - url: acf.get('ajaxurl'), - data: acf.prepareForAjax(ajaxData), + $.ajax( { + url: acf.get( 'ajaxurl' ), + data: acf.prepareForAjax( ajaxData ), type: 'post', dataType: 'html', success: step2, - }); + } ); }; // step 2. - var step2 = function (html) { + var step2 = function ( html ) { // update popup - popup.loading(false); - popup.content(html); + popup.loading( false ); + popup.content( html ); // vars - $form = popup.$('form'); - $name = popup.$('input[name="term_name"]'); - $parent = popup.$('select[name="term_parent"]'); - $button = popup.$('.acf-submit-button'); + $form = popup.$( 'form' ); + $name = popup.$( 'input[name="term_name"]' ); + $parent = popup.$( 'select[name="term_parent"]' ); + $button = popup.$( '.acf-submit-button' ); // focus - $name.trigger('focus'); + $name.trigger( 'focus' ); // submit form - popup.on('submit', 'form', step3); + popup.on( 'submit', 'form', step3 ); }; // step 3. - var step3 = function (e, $el) { + var step3 = function ( e, $el ) { // prevent e.preventDefault(); e.stopImmediatePropagation(); // basic validation - if ($name.val() === '') { - $name.trigger('focus'); + if ( $name.val() === '' ) { + $name.trigger( 'focus' ); return false; } // disable - acf.startButtonLoading($button); + acf.startButtonLoading( $button ); // ajax var ajaxData = { action: 'acf/fields/taxonomy/add_term', - field_key: field.get('key'), - nonce: field.get('nonce'), + field_key: field.get( 'key' ), + nonce: field.get( 'nonce' ), term_name: $name.val(), term_parent: $parent.length ? $parent.val() : 0, }; - $.ajax({ - url: acf.get('ajaxurl'), - data: acf.prepareForAjax(ajaxData), + $.ajax( { + url: acf.get( 'ajaxurl' ), + data: acf.prepareForAjax( ajaxData ), type: 'post', dataType: 'json', success: step4, - }); + } ); }; // step 4. - var step4 = function (json) { + var step4 = function ( json ) { // enable - acf.stopButtonLoading($button); + acf.stopButtonLoading( $button ); // remove prev notice - if (notice) { + if ( notice ) { notice.remove(); } // success - if (acf.isAjaxSuccess(json)) { + if ( acf.isAjaxSuccess( json ) ) { // clear name - $name.val(''); + $name.val( '' ); // update term lists - step5(json.data); + step5( json.data ); // notice - notice = acf.newNotice({ + notice = acf.newNotice( { type: 'success', - text: acf.getAjaxMessage(json), + text: acf.getAjaxMessage( json ), target: $form, timeout: 2000, dismiss: false, - }); + } ); } else { // notice - notice = acf.newNotice({ + notice = acf.newNotice( { type: 'error', - text: acf.getAjaxError(json), + text: acf.getAjaxError( json ), target: $form, timeout: 2000, dismiss: false, - }); + } ); } // focus - $name.trigger('focus'); + $name.trigger( 'focus' ); }; // step 5. - var step5 = function (term) { + var step5 = function ( term ) { // update parent dropdown var $option = $( '
; } @@ -229,7 +249,10 @@ const md5 = require( 'md5' ); // Remove all empty attribute defaults from PHP values to allow serialisation. // https://github.com/WordPress/gutenberg/issues/7342 for ( const key in blockType.attributes ) { - if ( 'default' in blockType.attributes[ key ] && blockType.attributes[ key ].default.length === 0 ) { + if ( + 'default' in blockType.attributes[ key ] && + blockType.attributes[ key ].default.length === 0 + ) { delete blockType.attributes[ key ].default; } } @@ -247,20 +270,41 @@ const md5 = require( 'md5' ); // Apply alignText functionality. if ( blockType.supports.alignText || blockType.supports.align_text ) { - blockType.attributes = addBackCompatAttribute( blockType.attributes, 'align_text', 'string' ); + blockType.attributes = addBackCompatAttribute( + blockType.attributes, + 'align_text', + 'string' + ); ThisBlockEdit = withAlignTextComponent( ThisBlockEdit, blockType ); } // Apply alignContent functionality. - if ( blockType.supports.alignContent || blockType.supports.align_content ) { - blockType.attributes = addBackCompatAttribute( blockType.attributes, 'align_content', 'string' ); - ThisBlockEdit = withAlignContentComponent( ThisBlockEdit, blockType ); + if ( + blockType.supports.alignContent || + blockType.supports.align_content + ) { + blockType.attributes = addBackCompatAttribute( + blockType.attributes, + 'align_content', + 'string' + ); + ThisBlockEdit = withAlignContentComponent( + ThisBlockEdit, + blockType + ); } // Apply fullHeight functionality. if ( blockType.supports.fullHeight || blockType.supports.full_height ) { - blockType.attributes = addBackCompatAttribute( blockType.attributes, 'full_height', 'boolean' ); - ThisBlockEdit = withFullHeightComponent( ThisBlockEdit, blockType.blockType ); + blockType.attributes = addBackCompatAttribute( + blockType.attributes, + 'full_height', + 'boolean' + ); + ThisBlockEdit = withFullHeightComponent( + ThisBlockEdit, + blockType.blockType + ); } // Set edit and save functions. @@ -269,7 +313,9 @@ const md5 = require( 'md5' ); wp.element.useEffect( () => { return () => { if ( ! wp.data.dispatch( 'core/editor' ) ) return; - wp.data.dispatch( 'core/editor' ).unlockPostSaving( 'acf/block/' + props.clientId ); + wp.data + .dispatch( 'core/editor' ) + .unlockPostSaving( 'acf/block/' + props.clientId ); }; }, [] ); @@ -307,7 +353,10 @@ const md5 = require( 'md5' ); */ function select( selector ) { if ( selector === 'core/block-editor' ) { - return wp.data.select( 'core/block-editor' ) || wp.data.select( 'core/editor' ); + return ( + wp.data.select( 'core/block-editor' ) || + wp.data.select( 'core/editor' ) + ); } return wp.data.select( selector ); } @@ -340,7 +389,9 @@ const md5 = require( 'md5' ); // Local function to recurse through all child blocks and add to the blocks array. const recurseBlocks = ( block ) => { blocks.push( block ); - select( 'core/block-editor' ).getBlocks( block.clientId ).forEach( recurseBlocks ); + select( 'core/block-editor' ) + .getBlocks( block.clientId ) + .forEach( recurseBlocks ); }; // Trigger initial recursion for parent level blocks. @@ -348,7 +399,9 @@ const md5 = require( 'md5' ); // Loop over args and filter. for ( const k in args ) { - blocks = blocks.filter( ( { attributes } ) => attributes[ k ] === args[ k ] ); + blocks = blocks.filter( + ( { attributes } ) => attributes[ k ] === args[ k ] + ); } // Return results. @@ -381,10 +434,18 @@ const md5 = require( 'md5' ); * @return object The AJAX promise. */ function fetchBlock( args ) { - const { attributes = {}, context = {}, query = {}, clientId = null, delay = 0 } = args; + const { + attributes = {}, + context = {}, + query = {}, + clientId = null, + delay = 0, + } = args; // Build a unique queue ID from block data, including the clientId for edit forms. - const queueId = md5( JSON.stringify( { ...attributes, ...context, ...query } ) ); + const queueId = md5( + JSON.stringify( { ...attributes, ...context, ...query } ) + ); const data = ajaxQueue[ queueId ] || { query: {}, @@ -404,7 +465,10 @@ const md5 = require( 'md5' ); data.started = true; if ( fetchCache[ queueId ] ) { ajaxQueue[ queueId ] = null; - data.promise.resolve.apply( fetchCache[ queueId ][ 0 ], fetchCache[ queueId ][ 1 ] ); + data.promise.resolve.apply( + fetchCache[ queueId ][ 0 ], + fetchCache[ queueId ][ 1 ] + ); } else { $.ajax( { url: acf.get( 'ajaxurl' ), @@ -468,7 +532,10 @@ const md5 = require( 'md5' ); // Apply a temporary wrapper for the jQuery parse to prevent text nodes triggering errors. html = '
' + html + '
'; // Correctly balance InnerBlocks tags for jQuery's initial parse. - html = html.replace( /]+)?\/>/, '' ); + html = html.replace( + /]+)?\/>/, + '' + ); return parseNode( $( html )[ 0 ], acfBlockVersion, 0 ).props.children; }; @@ -485,7 +552,10 @@ const md5 = require( 'md5' ); */ function parseNode( node, acfBlockVersion, level = 0 ) { // Get node name. - const nodeName = parseNodeName( node.nodeName.toLowerCase(), acfBlockVersion ); + const nodeName = parseNodeName( + node.nodeName.toLowerCase(), + acfBlockVersion + ); if ( ! nodeName ) { return null; } @@ -578,7 +648,10 @@ const md5 = require( 'md5' ); */ function ACFInnerBlocks( props ) { const { className = 'acf-innerblocks-container' } = props; - const innerBlockProps = useInnerBlocksProps( { className: className }, props ); + const innerBlockProps = useInnerBlocksProps( + { className: className }, + props + ); return
{ innerBlockProps.children }
; } @@ -597,7 +670,11 @@ const md5 = require( 'md5' ); let value = nodeAttr.value; // Allow overrides for third party libraries who might use specific attributes. - let shortcut = acf.applyFilters( 'acf_blocks_parse_node_attr', false, nodeAttr ); + let shortcut = acf.applyFilters( + 'acf_blocks_parse_node_attr', + false, + nodeAttr + ); if ( shortcut ) return shortcut; @@ -698,10 +775,13 @@ const md5 = require( 'md5' ); Object.keys( upgrades ).forEach( ( key ) => { if ( attributes[ key ] !== undefined ) { attributes[ upgrades[ key ] ] = attributes[ key ]; - } else if ( attributes[ upgrades[ key ] ] === undefined ) { + } else if ( + attributes[ upgrades[ key ] ] === undefined + ) { //Check for a default if ( blockType[ key ] !== undefined ) { - attributes[ upgrades[ key ] ] = blockType[ key ]; + attributes[ upgrades[ key ] ] = + blockType[ key ]; } } delete blockType[ key ]; @@ -710,7 +790,10 @@ const md5 = require( 'md5' ); // Set default attributes for those undefined. for ( let attribute in blockType.attributes ) { - if ( attributes[ attribute ] === undefined && blockType[ attribute ] !== undefined ) { + if ( + attributes[ attribute ] === undefined && + blockType[ attribute ] !== undefined + ) { attributes[ attribute ] = blockType[ attribute ]; } } @@ -721,7 +804,11 @@ const md5 = require( 'md5' ); }, 'withDefaultAttributes' ); - wp.hooks.addFilter( 'editor.BlockListBlock', 'acf/with-default-attributes', withDefaultAttributes ); + wp.hooks.addFilter( + 'editor.BlockListBlock', + 'acf/with-default-attributes', + withDefaultAttributes + ); /** * The BlockSave functional component. @@ -756,10 +843,7 @@ const md5 = require( 'md5' ); } } - if ( - isBlockInQueryLoop( clientId ) || - isSiteEditor() - ) { + if ( isBlockInQueryLoop( clientId ) || isSiteEditor() ) { restrictMode( [ 'preview' ] ); } else { switch ( blockType.mode ) { @@ -780,8 +864,7 @@ const md5 = require( 'md5' ); const { name, attributes, setAttributes, clientId } = this.props; const blockType = getBlockType( name ); const forcePreview = - isBlockInQueryLoop( clientId ) || - isSiteEditor(); + isBlockInQueryLoop( clientId ) || isSiteEditor(); let { mode } = attributes; if ( forcePreview ) { @@ -795,8 +878,12 @@ const md5 = require( 'md5' ); } // Configure toggle variables. - const toggleText = mode === 'preview' ? acf.__( 'Switch to Edit' ) : acf.__( 'Switch to Preview' ); - const toggleIcon = mode === 'preview' ? 'edit' : 'welcome-view-site'; + const toggleText = + mode === 'preview' + ? acf.__( 'Switch to Edit' ) + : acf.__( 'Switch to Preview' ); + const toggleIcon = + mode === 'preview' ? 'edit' : 'welcome-view-site'; function toggleMode() { setAttributes( { mode: mode === 'preview' ? 'edit' : 'preview', @@ -843,8 +930,12 @@ const md5 = require( 'md5' ); const { mode } = attributes; const index = useSelect( ( select ) => { - const rootClientId = select( 'core/block-editor' ).getBlockRootClientId( clientId ); - return select( 'core/block-editor' ).getBlockIndex( clientId, rootClientId ); + const rootClientId = + select( 'core/block-editor' ).getBlockRootClientId( clientId ); + return select( 'core/block-editor' ).getBlockIndex( + clientId, + rootClientId + ); } ); let showForm = true; @@ -865,7 +956,10 @@ const md5 = require( 'md5' ); acf.blockInstances[ clientId ].mode = mode; if ( ! isSelected ) { - if ( blockSupportsValidation( name ) && acf.blockInstances[ clientId ].validation_errors ) { + if ( + blockSupportsValidation( name ) && + acf.blockInstances[ clientId ].validation_errors + ) { additionalClasses += ' acf-block-has-validation-error'; } acf.blockInstances[ clientId ].has_been_deselected = true; @@ -907,7 +1001,11 @@ const md5 = require( 'md5' ); */ class Div extends Component { render() { - return
; + return ( +
+ ); } } @@ -1003,20 +1101,32 @@ const md5 = require( 'md5' ); } // Set HTML to the preloaded version. - preloadedBlocks[ blockId ].html = preloadedBlocks[ blockId ].html.replaceAll( blockId, clientId ); + preloadedBlocks[ blockId ].html = preloadedBlocks[ + blockId + ].html.replaceAll( blockId, clientId ); // Replace blockId in errors. - if ( preloadedBlocks[ blockId ].validation && preloadedBlocks[ blockId ].validation.errors ) { - preloadedBlocks[ blockId ].validation.errors = preloadedBlocks[ blockId ].validation.errors.map( - ( error ) => { - error.input = error.input.replaceAll( blockId, clientId ); - return error; - } - ); + if ( + preloadedBlocks[ blockId ].validation && + preloadedBlocks[ blockId ].validation.errors + ) { + preloadedBlocks[ blockId ].validation.errors = + preloadedBlocks[ blockId ].validation.errors.map( + ( error ) => { + error.input = error.input.replaceAll( + blockId, + clientId + ); + return error; + } + ); } // Return preloaded object. - acf.debug( 'Preload successful', preloadedBlocks[ blockId ] ); + acf.debug( + 'Preload successful', + preloadedBlocks[ blockId ] + ); return preloadedBlocks[ blockId ]; } } @@ -1030,10 +1140,11 @@ const md5 = require( 'md5' ); } setState( state ) { - acf.blockInstances[ this.props.clientId ][ this.constructor.name ] = { - ...this.state, - ...state, - }; + acf.blockInstances[ this.props.clientId ][ this.constructor.name ] = + { + ...this.state, + ...state, + }; // Update component state if subscribed. // - Allows AJAX callback to update store without modifying state of an unmounted component. @@ -1046,7 +1157,12 @@ const md5 = require( 'md5' ); Object.assign( {}, this ), this.props.clientId, this.constructor.name, - Object.assign( {}, acf.blockInstances[ this.props.clientId ][ this.constructor.name ] ) + Object.assign( + {}, + acf.blockInstances[ this.props.clientId ][ + this.constructor.name + ] + ) ); } @@ -1064,7 +1180,10 @@ const md5 = require( 'md5' ); }; if ( this.renderMethod === 'jsx' ) { - state.jsx = acf.parseJSX( html, getBlockVersion( this.props.name ) ); + state.jsx = acf.parseJSX( + html, + getBlockVersion( this.props.name ) + ); // Handle templates which don't contain any valid JSX parsable elements. if ( ! state.jsx ) { @@ -1072,12 +1191,17 @@ const md5 = require( 'md5' ); 'Your ACF block template contains no valid HTML elements. Appending a empty div to prevent React JS errors.' ); state.html += '
'; - state.jsx = acf.parseJSX( state.html, getBlockVersion( this.props.name ) ); + state.jsx = acf.parseJSX( + state.html, + getBlockVersion( this.props.name ) + ); } // If we've got an object (as an array) find the first valid React ref. if ( Array.isArray( state.jsx ) ) { - let refElement = state.jsx.find( ( element ) => React.isValidElement( element ) ); + let refElement = state.jsx.find( ( element ) => + React.isValidElement( element ) + ); state.ref = refElement.ref; } else { state.ref = state.jsx.ref; @@ -1138,7 +1262,10 @@ const md5 = require( 'md5' ); // This causes all instances to share the same state (cool), which unfortunately // pulls $el back and forth between the last rendered reusable block. // This simple fix leaves a "clone" behind :) - if ( $prevParent.length && $prevParent[ 0 ] !== $thisParent[ 0 ] ) { + if ( + $prevParent.length && + $prevParent[ 0 ] !== $thisParent[ 0 ] + ) { $prevParent.html( $el.clone() ); } } @@ -1213,11 +1340,17 @@ const md5 = require( 'md5' ); } isNotNewlyAdded() { - return acf.blockInstances[ this.props.clientId ].has_been_deselected || false; + return ( + acf.blockInstances[ this.props.clientId ].has_been_deselected || + false + ); } hasShownValidation() { - return acf.blockInstances[ this.props.clientId ].shown_validation || false; + return ( + acf.blockInstances[ this.props.clientId ].shown_validation || + false + ); } setShownValidation() { @@ -1225,7 +1358,8 @@ const md5 = require( 'md5' ); } setValidationErrors( errors ) { - acf.blockInstances[ this.props.clientId ].validation_errors = errors; + acf.blockInstances[ this.props.clientId ].validation_errors = + errors; } getValidationErrors() { @@ -1238,12 +1372,16 @@ const md5 = require( 'md5' ); lockBlockForSaving() { if ( ! wp.data.dispatch( 'core/editor' ) ) return; - wp.data.dispatch( 'core/editor' ).lockPostSaving( 'acf/block/' + this.props.clientId ); + wp.data + .dispatch( 'core/editor' ) + .lockPostSaving( 'acf/block/' + this.props.clientId ); } unlockBlockForSaving() { if ( ! wp.data.dispatch( 'core/editor' ) ) return; - wp.data.dispatch( 'core/editor' ).unlockPostSaving( 'acf/block/' + this.props.clientId ); + wp.data + .dispatch( 'core/editor' ) + .unlockPostSaving( 'acf/block/' + this.props.clientId ); } displayValidation( $formEl ) { @@ -1257,7 +1395,12 @@ const md5 = require( 'md5' ); } const errors = this.getValidationErrors(); - acf.debug( 'Starting handle validation', Object.assign( {}, this ), Object.assign( {}, $formEl ), errors ); + acf.debug( + 'Starting handle validation', + Object.assign( {}, this ), + Object.assign( {}, $formEl ), + errors + ); this.setShownValidation(); @@ -1323,8 +1466,15 @@ const md5 = require( 'md5' ); const preloaded = this.maybePreload( hash, clientId, true ); if ( preloaded ) { - this.setHtml( acf.applyFilters( 'blocks/form/render', preloaded.html, true ) ); - if ( preloaded.validation ) this.setValidationErrors( preloaded.validation.errors ); + this.setHtml( + acf.applyFilters( + 'blocks/form/render', + preloaded.html, + true + ) + ); + if ( preloaded.validation ) + this.setValidationErrors( preloaded.validation.errors ); return; } @@ -1342,20 +1492,31 @@ const md5 = require( 'md5' ); acf.debug( 'fetch block form promise' ); if ( ! data ) { - this.setHtml( `
${acf.__( 'Error loading block form' )}
` ); + this.setHtml( + `
${ acf.__( + 'Error loading block form' + ) }
` + ); return; } if ( data.form ) { this.setHtml( - acf.applyFilters( 'blocks/form/render', data.form.replaceAll( data.clientId, clientId ), false ) + acf.applyFilters( + 'blocks/form/render', + data.form.replaceAll( data.clientId, clientId ), + false + ) ); } - if ( data.validation ) this.setValidationErrors( data.validation.errors ); + if ( data.validation ) + this.setValidationErrors( data.validation.errors ); if ( this.isNotNewlyAdded() ) { - acf.debug( "Block has already shown it's invalid. The form needs to show validation errors" ); + acf.debug( + "Block has already shown it's invalid. The form needs to show validation errors" + ); this.validate(); } } ); @@ -1366,7 +1527,10 @@ const md5 = require( 'md5' ); this.loadState(); } - acf.debug( 'BlockForm calling validate with state', Object.assign( {}, this ) ); + acf.debug( + 'BlockForm calling validate with state', + Object.assign( {}, this ) + ); super.displayValidation( this.state.$el ); } @@ -1398,8 +1562,13 @@ const md5 = require( 'md5' ); const { $el } = this.state; - if ( blockSupportsValidation( this.props.name ) && this.isNotNewlyAdded() ) { - acf.debug( "Block has already shown it's invalid. The form needs to show validation errors" ); + if ( + blockSupportsValidation( this.props.name ) && + this.isNotNewlyAdded() + ) { + acf.debug( + "Block has already shown it's invalid. The form needs to show validation errors" + ); this.validate(); } @@ -1431,8 +1600,14 @@ const md5 = require( 'md5' ); } ); } - if ( blockSupportsValidation( name ) && ! silent && thisBlockForm.getMode() === 'edit' ) { - acf.debug( 'No block preview currently available. Need to trigger a validation only fetch.' ); + if ( + blockSupportsValidation( name ) && + ! silent && + thisBlockForm.getMode() === 'edit' + ) { + acf.debug( + 'No block preview currently available. Need to trigger a validation only fetch.' + ); thisBlockForm.fetch( true, data ); } } @@ -1508,10 +1683,20 @@ const md5 = require( 'md5' ); if ( preloaded ) { if ( getBlockVersion( name ) == 1 ) { - preloaded.html = '
' + preloaded.html + '
'; + preloaded.html = + '
' + + preloaded.html + + '
'; } - this.setHtml( acf.applyFilters( 'blocks/preview/render', preloaded.html, true ) ); - if ( preloaded.validation ) this.setValidationErrors( preloaded.validation.errors ); + this.setHtml( + acf.applyFilters( + 'blocks/preview/render', + preloaded.html, + true + ) + ); + if ( preloaded.validation ) + this.setValidationErrors( preloaded.validation.errors ); return; } @@ -1530,16 +1715,32 @@ const md5 = require( 'md5' ); delay, } ).done( ( { data } ) => { if ( ! data ) { - this.setHtml( `
${acf.__( 'Error previewing block' )}
` ); + this.setHtml( + `
${ acf.__( + 'Error previewing block' + ) }
` + ); return; } - let replaceHtml = data.preview.replaceAll( data.clientId, clientId ); + let replaceHtml = data.preview.replaceAll( + data.clientId, + clientId + ); if ( getBlockVersion( name ) == 1 ) { - replaceHtml = '
' + replaceHtml + '
'; + replaceHtml = + '
' + + replaceHtml + + '
'; } acf.debug( 'fetch block render promise' ); - this.setHtml( acf.applyFilters( 'blocks/preview/render', replaceHtml, false ) ); + this.setHtml( + acf.applyFilters( + 'blocks/preview/render', + replaceHtml, + false + ) + ); if ( data.validation ) { this.setValidationErrors( data.validation.errors ); } @@ -1582,7 +1783,9 @@ const md5 = require( 'md5' ); delay = 300; } - acf.debug( 'Triggering fetch from block preview shouldComponentUpdate' ); + acf.debug( + 'Triggering fetch from block preview shouldComponentUpdate' + ); this.fetch( { attributes: nextAttributes, @@ -1613,7 +1816,11 @@ const md5 = require( 'md5' ); // Do action. acf.doAction( 'render_block_preview', blockElement, attributes ); - acf.doAction( `render_block_preview/type=${ type }`, blockElement, attributes ); + acf.doAction( + `render_block_preview/type=${ type }`, + blockElement, + attributes + ); } componentDidRemount() { @@ -1629,10 +1836,15 @@ const md5 = require( 'md5' ); // Update preview if data has changed since last render (changing from "edit" to "preview"). if ( - ! compareObjects( this.state.prevAttributes, this.props.attributes ) || + ! compareObjects( + this.state.prevAttributes, + this.props.attributes + ) || ! compareObjects( this.state.prevContext, this.props.context ) ) { - acf.debug( 'Triggering block preview fetch from componentDidRemount' ); + acf.debug( + 'Triggering block preview fetch from componentDidRemount' + ); this.fetch(); } @@ -1714,7 +1926,9 @@ const md5 = require( 'md5' ); const DEFAULT = 'center center'; if ( align ) { const [ y, x ] = align.split( ' ' ); - return `${ validateVerticalAlignment( y ) } ${ validateHorizontalAlignment( x ) }`; + return `${ validateVerticalAlignment( + y + ) } ${ validateHorizontalAlignment( x ) }`; } return DEFAULT; } @@ -1731,12 +1945,14 @@ const md5 = require( 'md5' ); */ function withAlignContentComponent( OriginalBlockEdit, blockType ) { // Determine alignment vars - let type = blockType.supports.align_content || blockType.supports.alignContent; + let type = + blockType.supports.align_content || blockType.supports.alignContent; let AlignmentComponent; let validateAlignment; switch ( type ) { case 'matrix': - AlignmentComponent = BlockAlignmentMatrixControl || BlockAlignmentMatrixToolbar; + AlignmentComponent = + BlockAlignmentMatrixControl || BlockAlignmentMatrixToolbar; validateAlignment = validateMatrixAlignment; break; default: @@ -1747,7 +1963,9 @@ const md5 = require( 'md5' ); // Ensure alignment component exists. if ( AlignmentComponent === undefined ) { - console.warn( `The "${ type }" alignment component was not found.` ); + console.warn( + `The "${ type }" alignment component was not found.` + ); return OriginalBlockEdit; } @@ -1811,7 +2029,10 @@ const md5 = require( 'md5' ); return ( - + @@ -1848,7 +2069,10 @@ const md5 = require( 'md5' ); return ( - + From c67effcaac0387d0340dd7c931196c8d72e4ccd1 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 4 Nov 2025 13:32:05 +0100 Subject: [PATCH 16/22] Migrate 6-6-1 except blocks --- assets/src/js/_acf-field-color-picker.js | 7 +++- assets/src/js/_acf-tinymce.js | 4 +- assets/src/sass/acf-input.scss | 52 ++++++++++++++++++------ includes/blocks.php | 14 +++---- 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/assets/src/js/_acf-field-color-picker.js b/assets/src/js/_acf-field-color-picker.js index 46807526..3974c26e 100644 --- a/assets/src/js/_acf-field-color-picker.js +++ b/assets/src/js/_acf-field-color-picker.js @@ -63,7 +63,12 @@ // filter var args = acf.applyFilters( 'color_picker_args', args, this ); - + if ( Array.isArray( args.palettes ) && args.palettes.length > 10 ) { + // Add class for large custom palette styling + this.$control().addClass( + 'acf-color-picker-large-custom-palette' + ); + } // initialize $inputText.wpColorPicker( args ); }, diff --git a/assets/src/js/_acf-tinymce.js b/assets/src/js/_acf-tinymce.js index 3b66f8f7..d9e1637e 100644 --- a/assets/src/js/_acf-tinymce.js +++ b/assets/src/js/_acf-tinymce.js @@ -352,7 +352,9 @@ // Ensure textarea element is visible // - Fixes bug in block editor when switching between "Block" and "Document" tabs. - $( '#' + id ).show(); + if ( ! tinymce.get( id ) ) { + $( '#' + id ).show(); + } // toggle switchEditors.go( id, 'tmce' ); diff --git a/assets/src/sass/acf-input.scss b/assets/src/sass/acf-input.scss index 4cf97da5..cf41acf7 100644 --- a/assets/src/sass/acf-input.scss +++ b/assets/src/sass/acf-input.scss @@ -655,23 +655,51 @@ html[dir=rtl] input.acf-is-prepended.acf-is-appended { position: relative; z-index: 1; } +.acf-color-picker.acf-color-picker-large-custom-palette .iris-picker { + display: flex; + flex-direction: column; + height: inherit !important; +} + +.acf-color-picker.acf-color-picker-large-custom-palette .iris-picker .iris-picker-inner { + position: initial; + margin: 10px; +} + +.acf-color-picker.acf-color-picker-large-custom-palette .iris-picker .iris-palette-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 4px; + position: relative; + padding-top: 10px; +} + +.acf-color-picker.acf-color-picker-large-custom-palette .iris-picker .iris-palette-container .iris-palette { + width: 20px !important; + height: 20px !important; + margin: 0 !important; +} + +.acf-color-picker.acf-hide-color-picker-color-wheel:not(.acf-color-picker-large-custom-palette) .iris-picker { + width: inherit !important; + margin-right: 25px !important; +} + .acf-color-picker.acf-hide-color-picker-color-wheel .iris-picker { - width: inherit !important; - height: inherit !important; - padding: 10px !important; - margin-right: 99px; + height: inherit !important; + padding: 10px 0 !important; } -.acf-color-picker.acf-hide-color-picker-color-wheel .iris-palette-container { - display: flex; - position: relative; - left: inherit; - bottom: inherit; +.acf-color-picker.acf-hide-color-picker-color-wheel .iris-picker .iris-picker-inner { + display: none; } -.acf-color-picker.acf-hide-color-picker-color-wheel .iris-square, -.acf-color-picker.acf-hide-color-picker-color-wheel .iris-slider { - display: none; +.acf-color-picker.acf-hide-color-picker-color-wheel .iris-picker .iris-palette-container { + display: flex; + position: relative; + bottom: inherit; + padding-top: 0 !important; } /*----------------------------------------------------------------------------- diff --git a/includes/blocks.php b/includes/blocks.php index 4d792ba3..fddcaaa8 100644 --- a/includes/blocks.php +++ b/includes/blocks.php @@ -597,7 +597,10 @@ function acf_render_block_callback( $attributes, $content = '', $wp_block = null * @return string The block HTML. */ function acf_rendered_block( $attributes, $content = '', $is_preview = false, $post_id = 0, $wp_block = null, $context = false, $is_ajax_render = false ) { - if ( isset( $wp_block->block_type->acf_block_version ) && $wp_block->block_type->acf_block_version >= 3 ) { + $registry = WP_Block_Type_Registry::get_instance(); + $wp_block_type = $registry->get_registered( $attributes['name'] ); + + if ( isset( $wp_block_type->acf_block_version ) && $wp_block_type->acf_block_version >= 3 ) { $mode = 'preview'; $form = false; } else { @@ -1083,15 +1086,8 @@ function acf_ajax_fetch_block() { $content = ''; $is_preview = true; - $registry = WP_Block_Type_Registry::get_instance(); - $wp_block_type = $registry->get_registered( $block['name'] ); - - // We need to match what gets automatically passed to acf_rendered_block by WP core. - $wp_block = new stdClass(); - $wp_block->block_type = $wp_block_type; - // Render and store HTML. - $response['preview'] = acf_rendered_block( $block, $content, $is_preview, $post_id, $wp_block, $context, true ); + $response['preview'] = acf_rendered_block( $block, $content, $is_preview, $post_id, null, $context, true ); } // Send response. From 40843de64c81653b88735abdd5eb7041834cf2a9 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:57:27 +0100 Subject: [PATCH 17/22] Add 6.6.1 blocks changes --- .../js/pro/blocks-v3/components/block-edit.js | 141 ++++++++++++------ .../js/pro/blocks-v3/utils/post-locking.js | 64 +++++--- 2 files changed, 137 insertions(+), 68 deletions(-) diff --git a/assets/src/js/pro/blocks-v3/components/block-edit.js b/assets/src/js/pro/blocks-v3/components/block-edit.js index 5fb0c842..498d56cd 100644 --- a/assets/src/js/pro/blocks-v3/components/block-edit.js +++ b/assets/src/js/pro/blocks-v3/components/block-edit.js @@ -5,7 +5,13 @@ */ import md5 from 'md5'; -import { useState, useEffect, useRef, createPortal } from '@wordpress/element'; +import { + useState, + useEffect, + useRef, + createPortal, + useMemo, +} from '@wordpress/element'; import { BlockControls, @@ -27,8 +33,29 @@ import { lockPostSaving, unlockPostSaving, sortObjectKeys, + lockPostSavingByName, } from '../utils/post-locking'; +/** + * InspectorBlockFormContainer + * Small helper component that manages the inspector panel container ref + * Sets the current form container when the inspector panel is available + * + * @param {Object} props + * @param {React.RefObject} props.inspectorBlockFormRef - Ref to inspector container + * @param {Function} props.setCurrentBlockFormContainer - Setter for current container + */ +const InspectorBlockFormContainer = ( { + inspectorBlockFormRef, + setCurrentBlockFormContainer, +} ) => { + useEffect( () => { + setCurrentBlockFormContainer( inspectorBlockFormRef.current ); + }, [] ); + + return
; +}; + /** * Main BlockEdit component wrapper * Manages block data fetching and initial setup @@ -58,11 +85,18 @@ export const BlockEdit = ( props ) => { ); const [ userHasInteractedWithForm, setUserHasInteractedWithForm ] = useState( false ); + const [ hasFetchedOnce, setHasFetchedOnce ] = useState( false ); + const [ ajaxRequest, setAjaxRequest ] = useState(); const acfFormRef = useRef( null ); const previewRef = useRef( null ); const debounceRef = useRef( null ); + const attributesWithoutError = useMemo( () => { + const { hasAcfError, ...rest } = attributes; + return rest; + }, [ attributes ] ); + /** * Fetches block data from server (form HTML, preview HTML, validation) * @@ -80,6 +114,11 @@ export const BlockEdit = ( props ) => { } ) { if ( ! theAttributes ) return; + // NEW: Abort any pending request + if ( ajaxRequest ) { + ajaxRequest.abort(); + } + // Generate hash of attributes for preload cache lookup const attributesHash = generateAttributesHash( theAttributes, context ); @@ -106,12 +145,10 @@ export const BlockEdit = ( props ) => { const blockData = { ...theAttributes }; - wp.data - .dispatch( 'core/editor' ) - .lockPostSaving( 'acf-fetching-block' ); + lockPostSavingByName( 'acf-fetching-block' ); // Fetch block data via AJAX - $.ajax( { + const request = $.ajax( { url: acf.get( 'ajaxurl' ), dataType: 'json', type: 'post', @@ -125,9 +162,7 @@ export const BlockEdit = ( props ) => { } ), } ) .done( ( response ) => { - wp.data - .dispatch( 'core/editor' ) - .unlockPostSaving( 'acf-fetching-block' ); + unlockPostSavingByName( 'acf-fetching-block' ); setBlockFormHtml( response.data.form ); @@ -158,12 +193,14 @@ export const BlockEdit = ( props ) => { } else { setValidationErrors( null ); } + + setHasFetchedOnce( true ); } ) .fail( function () { - wp.data - .dispatch( 'core/editor' ) - .unlockPostSaving( 'acf-fetching-block' ); + setHasFetchedOnce( true ); + unlockPostSavingByName( 'acf-fetching-block' ); } ); + setAjaxRequest( request ); } /** @@ -332,29 +369,37 @@ export const BlockEdit = ( props ) => { clearTimeout( debounceRef.current ); debounceRef.current = setTimeout( () => { - handleFormDataUpdate(); + const parsedData = JSON.parse( theSerializedAcfData ); + + if ( ! parsedData ) { + return void fetchBlockData( { + theAttributes: attributesWithoutError, + theClientId: clientId, + theContext: context, + isSelected: isSelected, + } ); + } + + if ( + theSerializedAcfData === + JSON.stringify( attributesWithoutError.data ) + ) { + return void fetchBlockData( { + theAttributes: attributesWithoutError, + theClientId: clientId, + theContext: context, + isSelected: isSelected, + } ); + } + + // Use original attributes (with hasAcfError) when updating + const updatedAttributes = { + ...attributes, // ← Keep this as 'attributes', not 'attributesWithoutError' + data: { ...parsedData }, + }; + setAttributes( updatedAttributes ); }, 200 ); - }, [ theSerializedAcfData ] ); - - /** - * Updates block attributes when form data changes - */ - function handleFormDataUpdate() { - const parsedData = JSON.parse( theSerializedAcfData ); - if ( ! parsedData ) return; - if ( theSerializedAcfData === JSON.stringify( attributes.data ) ) - return; - - const updatedAttributes = { ...attributes, data: { ...parsedData } }; - setAttributes( updatedAttributes ); - - fetchBlockData( { - theAttributes: updatedAttributes, - theClientId: clientId, - theContext: context, - isSelected: isSelected, - } ); - } + }, [ theSerializedAcfData, attributesWithoutError ] ); // Trigger ACF actions when preview is rendered useEffect( () => { @@ -385,6 +430,7 @@ export const BlockEdit = ( props ) => { userHasInteractedWithForm={ userHasInteractedWithForm } setUserHasInteractedWithForm={ setUserHasInteractedWithForm } previewRef={ previewRef } + hasFetchedOnce={ hasFetchedOnce } /> ); }; @@ -410,10 +456,10 @@ function BlockEditInner( props ) { blockFetcher, userHasInteractedWithForm, previewRef, + hasFetchedOnce, } = props; const { clientId } = useBlockEditContext(); - const invisibleFormContainerRef = useRef(); const inspectorControlsRef = useRef(); const [ isModalOpen, setIsModalOpen ] = useState( false ); const modalFormContainerRef = useRef(); @@ -478,7 +524,10 @@ function BlockEditInner( props ) { { /* Inspector panel container */ } -
+ { /* Render form via portal when container is available */ } @@ -491,12 +540,14 @@ function BlockEditInner( props ) { clientId={ clientId } blockFormHtml={ blockFormHtml } onMount={ () => { - blockFetcher( { - theAttributes: attributes, - theClientId: clientId, - theContext: context, - isSelected: isSelected, - } ); + if ( ! hasFetchedOnce ) { + blockFetcher( { + theAttributes: attributes, + theClientId: clientId, + theContext: context, + isSelected: isSelected, + } ); + } } } onChange={ function ( $form ) { const serializedData = acf.serialize( @@ -524,15 +575,7 @@ function BlockEditInner( props ) { , currentFormContainer || inspectorControlsRef.current ) } - - { /* Hidden container for form when not in inspector/modal */ } <> -
- { /* Modal for editing block fields */ } { isModalOpen && ( { - if ( wp.data.dispatch( 'core/editor' ) ) { - wp.data - .dispatch( 'core/editor' ) - .lockPostSaving( 'acf/block/' + clientId ); + const dispatch = wp.data.dispatch( 'core/editor' ); + if ( dispatch ) { + dispatch.lockPostSaving( 'acf/block/' + clientId ); } }; /** - * Unlocks post saving in the WordPress editor - * Called when block operations are complete + * Unlocks post saving in the WordPress editor for a specific block + * Called when block operations are complete for a specific block instance * * @param {string} clientId - The block's client ID */ export const unlockPostSaving = ( clientId ) => { - if ( wp.data.dispatch( 'core/editor' ) ) { - wp.data - .dispatch( 'core/editor' ) - .unlockPostSaving( 'acf/block/' + clientId ); + const dispatch = wp.data.dispatch( 'core/editor' ); + if ( dispatch ) { + dispatch.unlockPostSaving( 'acf/block/' + clientId ); } }; /** - * Sorts an object's keys alphabetically + * Locks post saving with a custom lock name + * Used for global operations that aren't tied to a specific block + * + * @param {string} lockName - The name of the lock + */ +export const lockPostSavingByName = ( lockName ) => { + const dispatch = wp.data.dispatch( 'core/editor' ); + if ( dispatch ) { + dispatch.lockPostSaving( 'acf/block/' + lockName ); + } +}; + +/** + * Unlocks post saving with a custom lock name + * Used for global operations that aren't tied to a specific block + * + * @param {string} lockName - The name of the lock + */ +export const unlockPostSavingByName = ( lockName ) => { + const dispatch = wp.data.dispatch( 'core/editor' ); + if ( dispatch ) { + dispatch.unlockPostSaving( 'acf/block/' + lockName ); + } +}; + +/** + * Sorts an object's keys alphabetically and returns a new object * Used for consistent object serialization and comparison + * Ensures that objects with same properties in different order produce same hash * * @param {Object} obj - Object to sort - * @returns {Object} - New object with sorted keys + * @returns {Object} - New object with sorted keys in alphabetical order */ -export const sortObjectKeys = ( obj ) => - Object.keys( obj ) +export const sortObjectKeys = ( obj ) => { + return Object.keys( obj ) .sort() - .reduce( ( result, key ) => { - result[ key ] = obj[ key ]; - return result; + .reduce( ( sortedObj, key ) => { + sortedObj[ key ] = obj[ key ]; + return sortedObj; }, {} ); +}; From c4e5193973e46133d9fdacb912127152485aee05 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:55:45 +0100 Subject: [PATCH 18/22] Backport 6.6.2 --- assets/src/js/_acf-validation.js | 19 +- assets/src/js/_field-group-field.js | 6 + .../js/pro/blocks-v3/components/block-edit.js | 196 +++++++++++++++--- .../js/pro/blocks-v3/components/block-form.js | 2 + .../blocks-v3/components/block-placeholder.js | 32 ++- assets/src/sass/acf-input.scss | 4 + assets/src/sass/pro/_blocks.scss | 51 +++++ includes/blocks.php | 34 +-- package-lock.json | 34 +-- package.json | 3 +- 10 files changed, 297 insertions(+), 84 deletions(-) diff --git a/assets/src/js/_acf-validation.js b/assets/src/js/_acf-validation.js index 556e94cc..7723ab07 100644 --- a/assets/src/js/_acf-validation.js +++ b/assets/src/js/_acf-validation.js @@ -1081,7 +1081,6 @@ // Backup vars. var _this = this; var _args = arguments; - // Perform validation within a Promise. return new Promise( function ( resolve, reject ) { // Bail early if is autosave or preview. @@ -1140,6 +1139,7 @@ // Recursive function to check all blocks (including nested innerBlocks) for ACF validation errors function checkBlocksForErrors( blocks ) { + const errors = []; return new Promise( function ( resolve ) { // Iterate through each block blocks.forEach( ( block ) => { @@ -1167,13 +1167,8 @@ .togglePublishSidebar(); } - // Get the block's client ID - const blockClientId = block.clientId; - - // Select the block with the error in the editor - wp.data - .dispatch( 'core/block-editor' ) - .selectBlock( blockClientId ); + // Add block to errors array + errors.push( block ); // Dispatch a custom event to notify about the block with validation error document.dispatchEvent( @@ -1197,6 +1192,14 @@ } } ); + // If errors were found, select the first one + if ( errors.length > 0 ) { + const blockClientId = errors[ 0 ].clientId; + wp.data + .dispatch( 'core/block-editor' ) + .selectBlock( blockClientId ); + } + // No errors found, resolve with false return resolve( false ); } ); diff --git a/assets/src/js/_field-group-field.js b/assets/src/js/_field-group-field.js index 0c04f54f..a00417e3 100644 --- a/assets/src/js/_field-group-field.js +++ b/assets/src/js/_field-group-field.js @@ -698,6 +698,12 @@ forceSanitize = true; } + forceSanitize = acf.applyFilters( + 'convert_field_name_to_lowercase', + forceSanitize, + this + ); + // Sanitize the input value (force if needed) const sanitized = acf.strSanitize( $el.val(), forceSanitize ); diff --git a/assets/src/js/pro/blocks-v3/components/block-edit.js b/assets/src/js/pro/blocks-v3/components/block-edit.js index 498d56cd..1827c5c0 100644 --- a/assets/src/js/pro/blocks-v3/components/block-edit.js +++ b/assets/src/js/pro/blocks-v3/components/block-edit.js @@ -11,6 +11,8 @@ import { useRef, createPortal, useMemo, + Component, + createContext, } from '@wordpress/element'; import { @@ -20,6 +22,7 @@ import { useBlockEditContext, } from '@wordpress/block-editor'; import { + Button, ToolbarGroup, ToolbarButton, Placeholder, @@ -36,6 +39,92 @@ import { lockPostSavingByName, } from '../utils/post-locking'; +// Error Boundary Context +const ErrorBoundaryContext = createContext( null ); + +// Error Boundary Component +class ErrorBoundary extends Component { + constructor( props ) { + super( props ); + this.resetErrorBoundary = this.resetErrorBoundary.bind( this ); + this.state = { didCatch: false, error: null }; + } + + static getDerivedStateFromError( error ) { + return { didCatch: true, error: error }; + } + + resetErrorBoundary() { + const { error } = this.state; + if ( error !== null ) { + this.setState( { didCatch: false, error: null } ); + } + } + + componentDidCatch( error, errorInfo ) { + acf.debug( 'Block preview error caught:', error, errorInfo ); + } + + render() { + const { children, fallbackRender, FallbackComponent, fallback } = + this.props; + const { didCatch, error } = this.state; + + let content = children; + + if ( didCatch ) { + const errorProps = { + error: error, + resetErrorBoundary: this.resetErrorBoundary, + }; + + if ( typeof fallbackRender === 'function' ) { + content = fallbackRender( errorProps ); + } else if ( FallbackComponent ) { + content = ; + } else if ( fallback !== undefined ) { + content = fallback; + } else { + throw error; + } + } + + return ( + + { content } + + ); + } +} + +// Fallback component to show when preview errors +const BlockPreviewErrorFallback = ( { + setBlockFormModalOpen, + blockLabel, + error, +} ) => { + let errorMessage = null; + + if ( error ) { + acf.debug( 'Block preview error:', error ); + errorMessage = acf.__( 'Error previewing block v3' ); + } + + return ( + + ); +}; + /** * InspectorBlockFormContainer * Small helper component that manages the inspector panel container ref @@ -76,13 +165,31 @@ export const BlockEdit = ( props ) => { const shouldValidate = blockType.validate; const { clientId } = useBlockEditContext(); - const [ validationErrors, setValidationErrors ] = useState( null ); + const preloadedData = useMemo( () => { + return checkPreloadedData( + generateAttributesHash( attributes, context ), + clientId, + isSelected + ); + }, [] ); + + const [ validationErrors, setValidationErrors ] = useState( () => { + return preloadedData?.validation?.errors ?? null; + } ); + const [ showValidationErrors, setShowValidationErrors ] = useState( null ); const [ theSerializedAcfData, setTheSerializedAcfData ] = useState( null ); const [ blockFormHtml, setBlockFormHtml ] = useState( '' ); - const [ blockPreviewHtml, setBlockPreviewHtml ] = useState( - 'acf-block-preview-loading' - ); + const [ blockPreviewHtml, setBlockPreviewHtml ] = useState( () => { + if ( preloadedData?.html ) { + return acf.applyFilters( + 'blocks/preview/render', + preloadedData.html, + true + ); + } + return 'acf-block-preview-loading'; + } ); const [ userHasInteractedWithForm, setUserHasInteractedWithForm ] = useState( false ); const [ hasFetchedOnce, setHasFetchedOnce ] = useState( false ); @@ -353,6 +460,7 @@ export const BlockEdit = ( props ) => { 'acf/block/has-error', handleErrorEvent ); + unlockPostSaving( clientId ); }; }, [] ); @@ -486,6 +594,17 @@ function BlockEditInner( props ) { } }, [ isSelected, inspectorControlsRef, inspectorControlsRef.current ] ); + useEffect( () => { + if ( + isSelected && + validationErrors && + showValidationErrors && + blockType?.hide_fields_in_sidebar + ) { + setIsModalOpen( true ); + } + }, [ isSelected, showValidationErrors, validationErrors, blockType ] ); + // Build block CSS classes let blockClasses = 'acf-block-component acf-block-body'; blockClasses += ' acf-block-preview'; @@ -524,6 +643,17 @@ function BlockEditInner( props ) { { /* Inspector panel container */ } +
+ +
, currentFormContainer || inspectorControlsRef.current @@ -601,26 +737,38 @@ function BlockEditInner( props ) { blockPreviewHtml={ blockPreviewHtml } blockProps={ blockProps } > - { /* Show placeholder when no HTML */ } - { blockPreviewHtml === 'acf-block-preview-no-html' ? ( - - ) : null } - - { /* Show spinner while loading */ } - { blockPreviewHtml === 'acf-block-preview-loading' && ( - - - - ) } - - { /* Render actual preview HTML */ } - { blockPreviewHtml !== 'acf-block-preview-loading' && - blockPreviewHtml !== 'acf-block-preview-no-html' && - blockPreviewHtml && - acf.parseJSX( blockPreviewHtml ) } + ( + + ) } + > + { /* Show placeholder when no HTML */ } + { blockPreviewHtml === 'acf-block-preview-no-html' ? ( + + ) : null } + + { /* Show spinner while loading */ } + { blockPreviewHtml === 'acf-block-preview-loading' && ( + + + + ) } + + { /* Render actual preview HTML */ } + { blockPreviewHtml !== 'acf-block-preview-loading' && + blockPreviewHtml !== 'acf-block-preview-no-html' && + blockPreviewHtml && + acf.parseJSX( blockPreviewHtml ) } + diff --git a/assets/src/js/pro/blocks-v3/components/block-form.js b/assets/src/js/pro/blocks-v3/components/block-form.js index 7baf6ec8..be551aec 100644 --- a/assets/src/js/pro/blocks-v3/components/block-form.js +++ b/assets/src/js/pro/blocks-v3/components/block-form.js @@ -34,6 +34,7 @@ export const BlockForm = ( { acfFormRef, userHasInteractedWithForm, attributes, + hideFieldsInSidebar, } ) => { const [ formHtml, setFormHtml ] = useState( blockFormHtml ); const [ pendingChange, setPendingChange ] = useState( false ); @@ -272,6 +273,7 @@ export const BlockForm = ( {
( - } label={ blockLabel }> - - -); + + + ); +}; diff --git a/assets/src/sass/acf-input.scss b/assets/src/sass/acf-input.scss index cf41acf7..2300b8b4 100644 --- a/assets/src/sass/acf-input.scss +++ b/assets/src/sass/acf-input.scss @@ -3371,4 +3371,8 @@ body.is-dragging-metaboxes #acf_after_title-sortables { display: flow-root; min-height: 60px; margin-bottom: 3px !important; +} + +.editor-sidebar__panel .is-side #poststuff .acf-postbox .postbox-header { + margin-top: -1px } \ No newline at end of file diff --git a/assets/src/sass/pro/_blocks.scss b/assets/src/sass/pro/_blocks.scss index 16840548..280acaff 100644 --- a/assets/src/sass/pro/_blocks.scss +++ b/assets/src/sass/pro/_blocks.scss @@ -243,6 +243,16 @@ position: absolute; right: 0; } + + html[dir=rtl] .acf-block-form-modal { + right: auto; + left: 0 + } + + html[dir=rtl] .acf-block-form-modal .components-modal__header .components-button { + left: 0; + right: auto + } @keyframes components-modal__appear-animation { 0% { @@ -271,4 +281,45 @@ transform: scale(1); } } + + @keyframes components-modal__appear-animation-rtl { + 0% { + left: -20px; + opacity: 0; + transform: scale(1) + } + + to { + left: 0; + opacity: 1; + transform: scale(1) + } + } + + @keyframes components-modal__disappear-animation-rtl { + 0% { + left: 0; + opacity: 1; + transform: scale(1) + } + + to { + left: -20px; + opacity: 0; + transform: scale(1) + } + } + + html[dir=rtl] .acf-block-form-modal.components-modal__frame { + animation-name: components-modal__appear-animation-rtl !important + } + + html[dir=rtl] .acf-block-form-modal.is-closing { + animation-name: components-modal__disappear-animation-rtl !important + } +} + +.acf-blocks-open-expanded-editor-btn.has-text.has-icon { + width: 100%; + justify-content: center } \ No newline at end of file diff --git a/includes/blocks.php b/includes/blocks.php index fddcaaa8..baea5c0d 100644 --- a/includes/blocks.php +++ b/includes/blocks.php @@ -115,14 +115,15 @@ function acf_handle_json_block_registration( $settings, $metadata ) { // Map custom SCF properties from the SCF key, with localization. $property_mappings = array( - 'renderCallback' => 'render_callback', - 'renderTemplate' => 'render_template', - 'mode' => 'mode', - 'blockVersion' => 'acf_block_version', - 'postTypes' => 'post_types', - 'validate' => 'validate', - 'validateOnLoad' => 'validate_on_load', - 'usePostMeta' => 'use_post_meta', + 'renderCallback' => 'render_callback', + 'renderTemplate' => 'render_template', + 'mode' => 'mode', + 'blockVersion' => 'acf_block_version', + 'postTypes' => 'post_types', + 'validate' => 'validate', + 'validateOnLoad' => 'validate_on_load', + 'usePostMeta' => 'use_post_meta', + 'hideFieldsInSidebar' => 'hide_fields_in_sidebar', ); $textdomain = ! empty( $metadata['textdomain'] ) ? $metadata['textdomain'] : 'secure-custom-fields'; $i18n_schema = get_block_metadata_i18n_schema(); @@ -858,14 +859,17 @@ function acf_enqueue_block_assets() { // Localize text. acf_localize_text( array( - 'Switch to Edit' => __( 'Switch to Edit', 'secure-custom-fields' ), - 'Switch to Preview' => __( 'Switch to Preview', 'secure-custom-fields' ), - 'Change content alignment' => __( 'Change content alignment', 'secure-custom-fields' ), - 'Error previewing block' => __( 'An error occurred when loading the preview for this block.', 'secure-custom-fields' ), - 'Error loading block form' => __( 'An error occurred when loading the block in edit mode.', 'secure-custom-fields' ), - 'Edit Block' => __( 'Edit Block', 'secure-custom-fields' ), + 'Switch to Edit' => __( 'Switch to Edit', 'secure-custom-fields' ), + 'Switch to Preview' => __( 'Switch to Preview', 'secure-custom-fields' ), + 'Change content alignment' => __( 'Change content alignment', 'secure-custom-fields' ), + 'Error previewing block' => __( 'An error occurred when loading the preview for this block.', 'secure-custom-fields' ), + 'Error loading block form' => __( 'An error occurred when loading the block in edit mode.', 'secure-custom-fields' ), + 'Edit Block' => __( 'Edit Block', 'secure-custom-fields' ), + 'Open Expanded Editor' => __( 'Open Expanded Editor', 'secure-custom-fields' ), + 'Error previewing block v3' => __( 'The preview for this block couldn’t be loaded. Review its content or settings for issues.', 'secure-custom-fields' ), + 'ACF Block' => __( 'ACF Block', 'secure-custom-fields' ), /* translators: %s: Block type title */ - '%s settings' => __( '%s settings', 'secure-custom-fields' ), + '%s settings' => __( '%s settings', 'secure-custom-fields' ), ) ); diff --git a/package-lock.json b/package-lock.json index ae887fcb..afd23bf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "": { "dependencies": { "@wordpress/icons": "^10.26.0", - "dompurify": "3.2.7", + "dompurify": "3.3.0", "md5": "^2.3.0" }, "devDependencies": { @@ -25,6 +25,7 @@ "husky": "^9.1.7", "markdownlint-cli": "^0.39.0", "mini-css-extract-plugin": "^2.9.1", + "prettier": "npm:wp-prettier@3.0.3", "sass": "^1.79.5", "sass-loader": "^16.0.2", "sort-package-json": "^2.14.0", @@ -6402,23 +6403,6 @@ "node": "*" } }, - "node_modules/@wordpress/scripts/node_modules/prettier": { - "name": "wp-prettier", - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-3.0.3.tgz", - "integrity": "sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/@wordpress/scripts/node_modules/run-con": { "version": "1.2.12", "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.12.tgz", @@ -9962,9 +9946,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -17681,12 +17665,12 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "name": "wp-prettier", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-3.0.3.tgz", + "integrity": "sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, diff --git a/package.json b/package.json index 293d48fa..6421b42f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@wordpress/icons": "^10.26.0", - "dompurify": "3.2.7", + "dompurify": "3.3.0", "md5": "^2.3.0" }, "devDependencies": { @@ -31,6 +31,7 @@ "husky": "^9.1.7", "markdownlint-cli": "^0.39.0", "mini-css-extract-plugin": "^2.9.1", + "prettier": "npm:wp-prettier@3.0.3", "sass": "^1.79.5", "sass-loader": "^16.0.2", "sort-package-json": "^2.14.0", From 7de6e456fd26d791665ac8e871d6a54e26a1b410 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:40:53 +0100 Subject: [PATCH 19/22] More fixes --- assets/src/js/bindings/block-editor.js | 10 +++++----- assets/src/js/pro/blocks-v3/components/block-edit.js | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/assets/src/js/bindings/block-editor.js b/assets/src/js/bindings/block-editor.js index 76315566..34bed689 100644 --- a/assets/src/js/bindings/block-editor.js +++ b/assets/src/js/bindings/block-editor.js @@ -303,8 +303,8 @@ const withCustomControls = createHigherOrderComponent( ( BlockEdit ) => { }; }, 'withCustomControls' ); -addFilter( - 'editor.BlockEdit', - 'secure-custom-fields/with-custom-controls', - withCustomControls -); +// addFilter( +// 'editor.BlockEdit', +// 'secure-custom-fields/with-custom-controls', +// withCustomControls +// ); diff --git a/assets/src/js/pro/blocks-v3/components/block-edit.js b/assets/src/js/pro/blocks-v3/components/block-edit.js index 1827c5c0..f369713b 100644 --- a/assets/src/js/pro/blocks-v3/components/block-edit.js +++ b/assets/src/js/pro/blocks-v3/components/block-edit.js @@ -37,6 +37,7 @@ import { unlockPostSaving, sortObjectKeys, lockPostSavingByName, + unlockPostSavingByName, } from '../utils/post-locking'; // Error Boundary Context @@ -650,9 +651,9 @@ function BlockEditInner( props ) { onClick={ () => { setIsModalOpen( true ); } } - > - { acf.__( 'Open Expanded Editor' ) } - + text={ acf.__( 'Open Expanded Editor' ) } + icon="edit" + />
Date: Fri, 7 Nov 2025 13:04:47 +0100 Subject: [PATCH 20/22] Add unit test --- .../js/pro/blocks-v3/components/block-edit.js | 89 +---- .../blocks-v3/components/error-boundary.js | 93 +++++ jest.config.js | 19 ++ package-lock.json | 260 ++++++++++++++ package.json | 5 + .../block-edit-error-boundary.test.js | 317 ++++++++++++++++++ tests/js/setup-tests.js | 18 + 7 files changed, 713 insertions(+), 88 deletions(-) create mode 100644 assets/src/js/pro/blocks-v3/components/error-boundary.js create mode 100644 jest.config.js create mode 100644 tests/js/blocks-v3/block-edit-error-boundary.test.js create mode 100644 tests/js/setup-tests.js diff --git a/assets/src/js/pro/blocks-v3/components/block-edit.js b/assets/src/js/pro/blocks-v3/components/block-edit.js index f369713b..6a4ba378 100644 --- a/assets/src/js/pro/blocks-v3/components/block-edit.js +++ b/assets/src/js/pro/blocks-v3/components/block-edit.js @@ -11,8 +11,6 @@ import { useRef, createPortal, useMemo, - Component, - createContext, } from '@wordpress/element'; import { @@ -32,6 +30,7 @@ import { import { BlockPlaceholder } from './block-placeholder'; import { BlockForm } from './block-form'; import { BlockPreview } from './block-preview'; +import { ErrorBoundary, BlockPreviewErrorFallback } from './error-boundary'; import { lockPostSaving, unlockPostSaving, @@ -40,92 +39,6 @@ import { unlockPostSavingByName, } from '../utils/post-locking'; -// Error Boundary Context -const ErrorBoundaryContext = createContext( null ); - -// Error Boundary Component -class ErrorBoundary extends Component { - constructor( props ) { - super( props ); - this.resetErrorBoundary = this.resetErrorBoundary.bind( this ); - this.state = { didCatch: false, error: null }; - } - - static getDerivedStateFromError( error ) { - return { didCatch: true, error: error }; - } - - resetErrorBoundary() { - const { error } = this.state; - if ( error !== null ) { - this.setState( { didCatch: false, error: null } ); - } - } - - componentDidCatch( error, errorInfo ) { - acf.debug( 'Block preview error caught:', error, errorInfo ); - } - - render() { - const { children, fallbackRender, FallbackComponent, fallback } = - this.props; - const { didCatch, error } = this.state; - - let content = children; - - if ( didCatch ) { - const errorProps = { - error: error, - resetErrorBoundary: this.resetErrorBoundary, - }; - - if ( typeof fallbackRender === 'function' ) { - content = fallbackRender( errorProps ); - } else if ( FallbackComponent ) { - content = ; - } else if ( fallback !== undefined ) { - content = fallback; - } else { - throw error; - } - } - - return ( - - { content } - - ); - } -} - -// Fallback component to show when preview errors -const BlockPreviewErrorFallback = ( { - setBlockFormModalOpen, - blockLabel, - error, -} ) => { - let errorMessage = null; - - if ( error ) { - acf.debug( 'Block preview error:', error ); - errorMessage = acf.__( 'Error previewing block v3' ); - } - - return ( - - ); -}; - /** * InspectorBlockFormContainer * Small helper component that manages the inspector panel container ref diff --git a/assets/src/js/pro/blocks-v3/components/error-boundary.js b/assets/src/js/pro/blocks-v3/components/error-boundary.js new file mode 100644 index 00000000..2734f80e --- /dev/null +++ b/assets/src/js/pro/blocks-v3/components/error-boundary.js @@ -0,0 +1,93 @@ +/** + * Error Boundary Components for V3 Blocks + * Handles errors during block preview rendering, particularly from invalid HTML + */ + +import { Component, createContext } from '@wordpress/element'; +import { BlockPlaceholder } from './block-placeholder'; + +// Error Boundary Context +const ErrorBoundaryContext = createContext( null ); + +// Error Boundary Component +export class ErrorBoundary extends Component { + constructor( props ) { + super( props ); + this.resetErrorBoundary = this.resetErrorBoundary.bind( this ); + this.state = { didCatch: false, error: null }; + } + + static getDerivedStateFromError( error ) { + return { didCatch: true, error: error }; + } + + resetErrorBoundary() { + const { error } = this.state; + if ( error !== null ) { + this.setState( { didCatch: false, error: null } ); + } + } + + componentDidCatch( error, errorInfo ) { + acf.debug( 'Block preview error caught:', error, errorInfo ); + } + + render() { + const { children, fallbackRender, FallbackComponent, fallback } = + this.props; + const { didCatch, error } = this.state; + + let content = children; + + if ( didCatch ) { + const errorProps = { + error: error, + resetErrorBoundary: this.resetErrorBoundary, + }; + + if ( typeof fallbackRender === 'function' ) { + content = fallbackRender( errorProps ); + } else if ( FallbackComponent ) { + content = ; + } else if ( fallback !== undefined ) { + content = fallback; + } else { + throw error; + } + } + + return ( + + { content } + + ); + } +} + +// Fallback component to show when preview errors +export const BlockPreviewErrorFallback = ( { + setBlockFormModalOpen, + blockLabel, + error, +} ) => { + let errorMessage = null; + + if ( error ) { + acf.debug( 'Block preview error:', error ); + errorMessage = acf.__( 'Error previewing block v3' ); + } + + return ( + + ); +}; diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..b8dfd14c --- /dev/null +++ b/jest.config.js @@ -0,0 +1,19 @@ +/** + * Jest configuration for unit tests + */ +module.exports = { + ...require( '@wordpress/scripts/config/jest-unit.config' ), + testMatch: [ '**/tests/js/**/*.test.js', '**/tests/js/**/*.test.jsx' ], + setupFilesAfterEnv: [ '/tests/js/setup-tests.js' ], + testEnvironment: 'jsdom', + moduleNameMapper: { + '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + }, + collectCoverageFrom: [ + 'assets/src/js/**/*.{js,jsx}', + '!assets/src/js/**/*.min.js', + '!**/node_modules/**', + '!**/vendor/**', + ], + transformIgnorePatterns: [ 'node_modules/(?!(react-jsx-parser)/)' ], +}; diff --git a/package-lock.json b/package-lock.json index afd23bf8..e0f7e4dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,8 @@ "@babel/core": "^7.25.8", "@babel/preset-env": "^7.25.8", "@babel/preset-react": "^7.25.7", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^14.1.2", "@wordpress/dependency-extraction-webpack-plugin": "^6.20.0", "@wordpress/e2e-test-utils-playwright": "^1.20.0", "@wordpress/icons": "^10.26.0", @@ -23,6 +25,7 @@ "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", "husky": "^9.1.7", + "identity-obj-proxy": "^3.0.0", "markdownlint-cli": "^0.39.0", "mini-css-extract-plugin": "^2.9.1", "prettier": "npm:wp-prettier@3.0.3", @@ -39,6 +42,13 @@ "npm": ">=10.8.1" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -4578,6 +4588,117 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/react": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", + "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4605,6 +4726,13 @@ "node": ">=10.13.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -9309,6 +9437,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -9655,6 +9790,39 @@ } } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -9887,6 +10055,13 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -10275,6 +10450,27 @@ "node": ">= 0.4" } }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-iterator-helpers": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", @@ -12442,6 +12638,13 @@ "node": ">=6" } }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true, + "license": "(Apache-2.0 OR MPL-1.1)" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -12865,6 +13068,19 @@ "postcss": "^8.1.0" } }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "license": "MIT", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -13134,6 +13350,23 @@ "node": ">=8" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -15174,6 +15407,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -16098,6 +16341,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", diff --git a/package.json b/package.json index 6421b42f..dcaf2883 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "sort-package-json": "sort-package-json", "test:e2e": "wp-scripts test-playwright", "test:e2e:debug": "wp-scripts test-playwright --debug", + "test:unit": "wp-scripts test-unit-js", + "test:unit:watch": "wp-scripts test-unit-js --watch", "watch": "webpack --watch" }, "dependencies": { @@ -19,6 +21,8 @@ "@babel/core": "^7.25.8", "@babel/preset-env": "^7.25.8", "@babel/preset-react": "^7.25.7", + "@testing-library/jest-dom": "^6.1.5", + "@testing-library/react": "^14.1.2", "@wordpress/dependency-extraction-webpack-plugin": "^6.20.0", "@wordpress/e2e-test-utils-playwright": "^1.20.0", "@wordpress/icons": "^10.26.0", @@ -29,6 +33,7 @@ "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", "husky": "^9.1.7", + "identity-obj-proxy": "^3.0.0", "markdownlint-cli": "^0.39.0", "mini-css-extract-plugin": "^2.9.1", "prettier": "npm:wp-prettier@3.0.3", diff --git a/tests/js/blocks-v3/block-edit-error-boundary.test.js b/tests/js/blocks-v3/block-edit-error-boundary.test.js new file mode 100644 index 00000000..b294c4f7 --- /dev/null +++ b/tests/js/blocks-v3/block-edit-error-boundary.test.js @@ -0,0 +1,317 @@ +/** + * Unit tests for ErrorBoundary component in V3 Blocks + * Tests the fallback behavior when block preview rendering fails due to invalid HTML + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +// Mock BlockPlaceholder before importing the components that use it +jest.mock( + '../../../assets/src/js/pro/blocks-v3/components/block-placeholder', + () => ( { + BlockPlaceholder: ( { blockLabel, instructions } ) => ( +
+
{ blockLabel }
+ { instructions && ( +
{ instructions }
+ ) } +
+ ), + } ) +); + +import { + ErrorBoundary, + BlockPreviewErrorFallback, +} from '../../../assets/src/js/pro/blocks-v3/components/error-boundary'; + +// Mock the acf global object +global.acf = { + __: jest.fn( ( key ) => { + const translations = { + 'Error previewing block v3': + "The preview for this block couldn't be loaded. Review its content or settings for issues.", + 'ACF Block': 'ACF Block', + }; + return translations[ key ] || key; + } ), + debug: jest.fn(), + parseJSX: jest.fn(), +}; + +// Component that throws an error (simulating invalid HTML parsing) +const ThrowError = ( { shouldThrow } ) => { + if ( shouldThrow ) { + throw new Error( 'Invalid HTML: Unclosed tag detected' ); + } + return
Valid preview content
; +}; + +describe( 'ErrorBoundary Component', () => { + beforeEach( () => { + jest.clearAllMocks(); + // Suppress console.error for these tests since we're intentionally throwing errors + jest.spyOn( console, 'error' ).mockImplementation( () => {} ); + } ); + + afterEach( () => { + console.error.mockRestore(); + } ); + + test( 'renders children normally when no error occurs', () => { + render( + + + + ); + + expect( + screen.getByText( 'Valid preview content' ) + ).toBeInTheDocument(); + } ); + + test( 'catches error and renders fallback when child component throws', () => { + const mockSetModalOpen = jest.fn(); + + render( + ( + + ) } + > + + + ); + + // Should render the error placeholder + expect( screen.getByTestId( 'block-placeholder' ) ).toBeInTheDocument(); + expect( screen.getByTestId( 'block-label' ) ).toHaveTextContent( + 'Test Block' + ); + expect( screen.getByTestId( 'error-message' ) ).toHaveTextContent( + "The preview for this block couldn't be loaded" + ); + } ); + + test( 'calls acf.debug when error is caught', () => { + const mockSetModalOpen = jest.fn(); + + render( + ( + + ) } + > + + + ); + + // Verify debug was called + expect( global.acf.debug ).toHaveBeenCalledWith( + 'Block preview error caught:', + expect.any( Error ), + expect.any( Object ) + ); + + expect( global.acf.debug ).toHaveBeenCalledWith( + 'Block preview error:', + expect.any( Error ) + ); + } ); + + test( 'displays correct error message from translation', () => { + const mockSetModalOpen = jest.fn(); + + render( + ( + + ) } + > + + + ); + + // Verify translation function was called + expect( global.acf.__ ).toHaveBeenCalledWith( + 'Error previewing block v3' + ); + + // Verify the translated message appears + expect( screen.getByTestId( 'error-message' ) ).toHaveTextContent( + "The preview for this block couldn't be loaded. Review its content or settings for issues." + ); + } ); + + test( 'uses fallback block label when blockType title is not available', () => { + const mockSetModalOpen = jest.fn(); + + render( + ( + + ) } + > + + + ); + + expect( screen.getByTestId( 'block-label' ) ).toHaveTextContent( + 'ACF Block' + ); + } ); +} ); + +describe( 'BlockPreviewErrorFallback Component', () => { + test( 'renders placeholder with error message when error is provided', () => { + const mockError = new Error( 'Invalid HTML' ); + const mockSetModalOpen = jest.fn(); + + render( + + ); + + expect( screen.getByTestId( 'block-placeholder' ) ).toBeInTheDocument(); + expect( screen.getByTestId( 'error-message' ) ).toBeInTheDocument(); + } ); + + test( 'does not render error message when error is null', () => { + const mockSetModalOpen = jest.fn(); + + render( + + ); + + expect( screen.getByTestId( 'block-placeholder' ) ).toBeInTheDocument(); + expect( + screen.queryByTestId( 'error-message' ) + ).not.toBeInTheDocument(); + } ); +} ); + +describe( 'Invalid HTML Scenarios', () => { + test( 'handles error from parseJSX with malformed HTML', () => { + // Simulate parseJSX throwing an error with invalid HTML + global.acf.parseJSX.mockImplementation( ( html ) => { + if ( html.includes( '

Unclosed' ) ) { + throw new Error( 'jQuery parsing error: Unclosed tag' ); + } + return

{ html }
; + } ); + + const InvalidHTMLComponent = () => { + const html = '

Unclosed'; + return global.acf.parseJSX( html ); + }; + + const mockSetModalOpen = jest.fn(); + + render( + ( + + ) } + > + + + ); + + expect( screen.getByTestId( 'block-placeholder' ) ).toBeInTheDocument(); + expect( screen.getByTestId( 'error-message' ) ).toHaveTextContent( + "The preview for this block couldn't be loaded" + ); + } ); + + test( 'handles error from parseJSX with script injection attempt', () => { + global.acf.parseJSX.mockImplementation( ( html ) => { + if ( html.includes( '

'; + return global.acf.parseJSX( html ); + }; + + const mockSetModalOpen = jest.fn(); + + render( + ( + + ) } + > + + + ); + + expect( screen.getByTestId( 'block-placeholder' ) ).toBeInTheDocument(); + expect( global.acf.debug ).toHaveBeenCalled(); + } ); + + test( 'renders successfully with valid HTML', () => { + global.acf.parseJSX.mockImplementation( ( html ) => { + return
; + } ); + + const ValidHTMLComponent = () => { + const html = '

This is valid HTML

'; + return global.acf.parseJSX( html ); + }; + + render( + ( + + ) } + > + + + ); + + // Should not show error placeholder + expect( + screen.queryByTestId( 'block-placeholder' ) + ).not.toBeInTheDocument(); + } ); +} ); diff --git a/tests/js/setup-tests.js b/tests/js/setup-tests.js new file mode 100644 index 00000000..81f07e0e --- /dev/null +++ b/tests/js/setup-tests.js @@ -0,0 +1,18 @@ +/** + * Jest test setup file + * Runs before all tests to set up the testing environment + */ + +// Add React to global scope +import React from 'react'; +global.React = React; + +// Mock jQuery for parseJSX tests +global.jQuery = jest.fn( ( html ) => { + if ( typeof html === 'string' ) { + // Simple mock that returns an array-like object + return [ { innerHTML: html, tagName: 'DIV' } ]; + } + return []; +} ); +global.$ = global.jQuery; From d42f49ebe8b8a525dd32dc1e7aaf25ae731da6b6 Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Fri, 7 Nov 2025 13:15:29 +0100 Subject: [PATCH 21/22] V3 Blocks now save default field values even if the block wasn't interacted with before saving --- .../js/pro/blocks-v3/components/block-form.js | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/assets/src/js/pro/blocks-v3/components/block-form.js b/assets/src/js/pro/blocks-v3/components/block-form.js index be551aec..24e744c8 100644 --- a/assets/src/js/pro/blocks-v3/components/block-form.js +++ b/assets/src/js/pro/blocks-v3/components/block-form.js @@ -40,24 +40,35 @@ export const BlockForm = ( { const [ pendingChange, setPendingChange ] = useState( false ); const debounceTimer = useRef( null ); const [ userInteracted, setUserInteracted ] = useState( false ); + const [ initialValuesCaptured, setInitialValuesCaptured ] = + useState( false ); // Call onMount when component first mounts useEffect( () => { onMount(); }, [] ); - // Trigger onChange when there's a pending change and user has interacted + // Trigger onChange when there's a pending change useEffect( () => { - if ( - pendingChange && - ( userHasInteractedWithForm || userInteracted ) - ) { - onChange( pendingChange ); - setPendingChange( false ); + if ( pendingChange ) { + // For the first change, capture default values even without interaction + if ( + ! initialValuesCaptured || + userHasInteractedWithForm || + userInteracted + ) { + onChange( pendingChange ); + setPendingChange( false ); + if ( ! initialValuesCaptured ) { + setInitialValuesCaptured( true ); + } + } } }, [ pendingChange, userHasInteractedWithForm, + userInteracted, + initialValuesCaptured, setPendingChange, onChange, ] ); From f35edae3c028fa77d599dad2d7811640e424599e Mon Sep 17 00:00:00 2001 From: Carlos Bravo <37012961+cbravobernal@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:53:16 +0100 Subject: [PATCH 22/22] Add more changes --- assets/src/js/pro/_acf-blocks.js | 10 ++- .../js/pro/blocks-v3/components/block-edit.js | 21 ++++- .../js/pro/blocks-v3/components/block-form.js | 19 ++--- .../blocks-v3/components/error-boundary.js | 80 +++++++++++++++---- 4 files changed, 100 insertions(+), 30 deletions(-) diff --git a/assets/src/js/pro/_acf-blocks.js b/assets/src/js/pro/_acf-blocks.js index dc9e7db5..91748001 100644 --- a/assets/src/js/pro/_acf-blocks.js +++ b/assets/src/js/pro/_acf-blocks.js @@ -1138,7 +1138,6 @@ const md5 = require( 'md5' ); const client = acf.blockInstances[ this.props.clientId ] || {}; this.state = client[ this.constructor.name ] || {}; } - setState( state ) { acf.blockInstances[ this.props.clientId ][ this.constructor.name ] = { @@ -1148,7 +1147,7 @@ const md5 = require( 'md5' ); // Update component state if subscribed. // - Allows AJAX callback to update store without modifying state of an unmounted component. - if ( this.subscribed ) { + if ( this.subscribed || acf.get( 'StrictMode' ) ) { super.setState( state ); } @@ -1313,9 +1312,12 @@ const md5 = require( 'md5' ); } componentWillUnmount() { - acf.doAction( 'unmount', this.state.$el ); + // Only skip unmount action if in StrictMode AND component is not subscribed + if ( ! acf.get( 'StrictMode' ) || this.subscribed ) { + acf.doAction( 'unmount', this.state.$el ); + } - // Unsubscribe this component from state. + // Unsubscribe this component from state this.subscribed = false; } diff --git a/assets/src/js/pro/blocks-v3/components/block-edit.js b/assets/src/js/pro/blocks-v3/components/block-edit.js index 6a4ba378..d1dc15f9 100644 --- a/assets/src/js/pro/blocks-v3/components/block-edit.js +++ b/assets/src/js/pro/blocks-v3/components/block-edit.js @@ -416,7 +416,7 @@ export const BlockEdit = ( props ) => { // Use original attributes (with hasAcfError) when updating const updatedAttributes = { - ...attributes, // ← Keep this as 'attributes', not 'attributesWithoutError' + ...attributes, data: { ...parsedData }, }; setAttributes( updatedAttributes ); @@ -661,6 +661,25 @@ function BlockEditInner( props ) { error={ error } /> ) } + onError={ ( error, errorInfo ) => { + acf.debug( + 'Block preview error caught:', + error, + errorInfo + ); + } } + resetKeys={ [ blockPreviewHtml ] } + onReset={ ( { reason, next, prev } ) => { + acf.debug( 'Error boundary reset:', reason ); + if ( reason === 'keys' ) { + acf.debug( + 'Preview HTML changed from', + prev, + 'to', + next + ); + } + } } > { /* Show placeholder when no HTML */ } { blockPreviewHtml === 'acf-block-preview-no-html' ? ( diff --git a/assets/src/js/pro/blocks-v3/components/block-form.js b/assets/src/js/pro/blocks-v3/components/block-form.js index 24e744c8..35b04b4b 100644 --- a/assets/src/js/pro/blocks-v3/components/block-form.js +++ b/assets/src/js/pro/blocks-v3/components/block-form.js @@ -140,14 +140,11 @@ export const BlockForm = ( { ); } - if ( block.attributes.hasAcfError ) { - const errorBlockClientId = block.clientId; - if ( errorBlockClientId !== clientId ) { - wp.data - .dispatch( 'core/block-editor' ) - .selectBlock( errorBlockClientId ); - return resolve( true ); - } + if ( + block.attributes.hasAcfError && + block.clientId !== clientId + ) { + return resolve( true ); } } ); return resolve( false ); @@ -187,6 +184,10 @@ export const BlockForm = ( { let isActive = true; acf.doAction( 'remount', $form ); + if ( ! initialValuesCaptured ) { + onChange( $form ); + setInitialValuesCaptured( true ); + } const handleChange = () => { onChange( $form ); @@ -213,7 +214,7 @@ export const BlockForm = ( { if ( isActive ) { setPendingChange( $form ); } - }, 200 ); + }, 300 ); }; // Observe DOM changes to detect field additions/removals diff --git a/assets/src/js/pro/blocks-v3/components/error-boundary.js b/assets/src/js/pro/blocks-v3/components/error-boundary.js index 2734f80e..ecc35cb3 100644 --- a/assets/src/js/pro/blocks-v3/components/error-boundary.js +++ b/assets/src/js/pro/blocks-v3/components/error-boundary.js @@ -1,20 +1,17 @@ -/** - * Error Boundary Components for V3 Blocks - * Handles errors during block preview rendering, particularly from invalid HTML - */ - import { Component, createContext } from '@wordpress/element'; -import { BlockPlaceholder } from './block-placeholder'; -// Error Boundary Context -const ErrorBoundaryContext = createContext( null ); +// Create context outside the class +export const ErrorBoundaryContext = createContext( null ); + +// Initial state constant +const initialState = { didCatch: false, error: null }; // Error Boundary Component export class ErrorBoundary extends Component { constructor( props ) { super( props ); this.resetErrorBoundary = this.resetErrorBoundary.bind( this ); - this.state = { didCatch: false, error: null }; + this.state = initialState; } static getDerivedStateFromError( error ) { @@ -24,12 +21,47 @@ export class ErrorBoundary extends Component { resetErrorBoundary() { const { error } = this.state; if ( error !== null ) { - this.setState( { didCatch: false, error: null } ); + // Collect all arguments passed to reset + const args = Array.from( arguments ); + + // Call optional onReset callback with context + if ( this.props.onReset ) { + this.props.onReset( { + args: args, + reason: 'imperative-api', + } ); + } + + this.setState( initialState ); } } componentDidCatch( error, errorInfo ) { - acf.debug( 'Block preview error caught:', error, errorInfo ); + // Call optional onError callback + if ( this.props.onError ) { + this.props.onError( error, errorInfo ); + } + } + + componentDidUpdate( prevProps, prevState ) { + const { didCatch } = this.state; + const { resetKeys } = this.props; + + // Auto-reset if resetKeys prop changed + if ( + didCatch && + prevState.error !== null && + hasResetKeysChanged( prevProps.resetKeys, resetKeys ) + ) { + if ( this.props.onReset ) { + this.props.onReset( { + next: resetKeys, + prev: prevProps.resetKeys, + reason: 'keys', + } ); + } + this.setState( initialState ); + } } render() { @@ -70,7 +102,14 @@ export class ErrorBoundary extends Component { } } -// Fallback component to show when preview errors +// Helper function to check if reset keys changed +function hasResetKeysChanged( prevKeys = [], nextKeys = [] ) { + return ( + prevKeys.length !== nextKeys.length || + prevKeys.some( ( key, index ) => ! Object.is( key, nextKeys[ index ] ) ) + ); +} + export const BlockPreviewErrorFallback = ( { setBlockFormModalOpen, blockLabel, @@ -84,10 +123,19 @@ export const BlockPreviewErrorFallback = ( { } return ( - } + label={ blockLabel } instructions={ errorMessage } - /> + > + + ); };