From 1b6e83e38908a1dd0aa5f32d754eb154d1c58917 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Thu, 24 Sep 2026 13:39:01 -0300 Subject: [PATCH 1/9] Improve subfield error handling --- .../controllers/FrmComboFieldsController.php | 8 + classes/controllers/FrmFieldsController.php | 9 +- classes/models/fields/FrmFieldCombo.php | 101 ++++++++- css/_single_theme.css.php | 10 + js/formidable.js | 207 +++++++++++++++++- tests/phpunit/fields/test_FrmFieldAddress.php | 45 +++- tests/phpunit/fields/test_FrmFieldName.php | 45 ++++ 7 files changed, 407 insertions(+), 18 deletions(-) diff --git a/classes/controllers/FrmComboFieldsController.php b/classes/controllers/FrmComboFieldsController.php index 2f8f0e2595..7bbe702aaa 100644 --- a/classes/controllers/FrmComboFieldsController.php +++ b/classes/controllers/FrmComboFieldsController.php @@ -121,6 +121,14 @@ public static function add_atts_to_input( $atts ) { $atts['sub_field']['atts']['disabled'] = 'disabled'; } + // Lets FrmFieldsController::add_shortcodes_to_html() pick this sub field's own aria-invalid + // value, and name this sub field in its required message for JS validation. + $atts['field']['subfield_name'] = $atts['key']; + + if ( $field_obj instanceof FrmFieldCombo ) { + $atts['field']['subfield_label'] = $field_obj->get_sub_field_label( $atts['key'] ); + } + if ( ! empty( $atts['sub_field']['optional'] ) ) { add_filter( 'frm_field_classes', 'FrmAddressesController::add_optional_class', 20, 2 ); do_action( 'frm_field_input_html', $atts['field'] ); diff --git a/classes/controllers/FrmFieldsController.php b/classes/controllers/FrmFieldsController.php index c67d58dd99..1dd8a6364f 100644 --- a/classes/controllers/FrmFieldsController.php +++ b/classes/controllers/FrmFieldsController.php @@ -957,7 +957,14 @@ private static function add_validation_messages( $field, array &$add_html ) { $field_validation_messages_status = self::get_validation_data_attribute_visibility_info( $field ); if ( FrmField::is_required( $field ) && ! empty( $field_validation_messages_status['data-reqmsg'] ) ) { - $required_message = FrmFieldsHelper::get_error_msg( $field, 'blank' ); + $required_field = $field; + + if ( ! empty( $field['subfield_label'] ) ) { + // A combo sub field, like a first name, is named on its own in its required message. + $required_field['name'] = $field['subfield_label']; + } + + $required_message = FrmFieldsHelper::get_error_msg( $required_field, 'blank' ); $add_html['data-reqmsg'] = 'data-reqmsg="' . esc_attr( $required_message ) . '"'; self::maybe_add_html_required( $field, $add_html ); } diff --git a/classes/models/fields/FrmFieldCombo.php b/classes/models/fields/FrmFieldCombo.php index ae3c872974..d012a17180 100644 --- a/classes/models/fields/FrmFieldCombo.php +++ b/classes/models/fields/FrmFieldCombo.php @@ -404,7 +404,8 @@ protected function print_input_atts( $args ) { $field['default_value'] = ''; if ( ! empty( $sub_field['name'] ) ) { - $field['subfield_name'] = $sub_field['name']; + $field['subfield_name'] = $sub_field['name']; + $field['subfield_label'] = $this->get_sub_field_label( $sub_field['name'] ); } do_action( 'frm_field_input_html', $field ); @@ -437,22 +438,97 @@ public function validate( $args ) { return $errors; } - $blank_msg = FrmFieldsHelper::get_error_msg( $this->field, 'blank' ); - $sub_fields = $this->get_processed_sub_fields(); + $required_count = 0; + $missing = array(); - // Validate not empty. - foreach ( $sub_fields as $name => $sub_field ) { - if ( ! empty( $sub_field['optional'] ) || ! empty( $args['value'][ $name ] ) ) { + foreach ( $this->get_processed_sub_fields() as $name => $sub_field ) { + if ( ! empty( $sub_field['optional'] ) ) { continue; } - $errors[ 'field' . $args['id'] . '-' . $name ] = ''; - $errors[ 'field' . $args['id'] ] = $blank_msg; + ++$required_count; + + if ( empty( $args['value'][ $name ] ) ) { + $missing[] = $name; + } + } + + if ( ! $missing ) { + return $errors; + } + + if ( count( $missing ) === $required_count ) { + // Nothing was filled in, so show one error for the whole field. The empty sub field + // errors flag each required input without repeating the message under every one. + foreach ( $missing as $name ) { + $errors[ 'field' . $args['id'] . '-' . $name ] = ''; + } + + $errors[ 'field' . $args['id'] ] = FrmFieldsHelper::get_error_msg( $this->field, 'blank' ); + + return $errors; + } + + // Only some sub fields are missing, so name each one in its own error. + foreach ( $missing as $name ) { + $errors[ 'field' . $args['id'] . '-' . $name ] = $this->get_sub_field_error_msg( $name, 'blank' ); } return $errors; } + /** + * Gets the label a sub field is referred to by in its error messages. + * + * The sub field description is used when there is one, since that is the label shown under + * the input. Otherwise the sub field label is combined with the field label, like "Address Line 1". + * + * @since x.x + * + * @param string $name Sub field name, like 'first' or 'line1'. + * + * @return string + */ + public function get_sub_field_label( $name ) { + $desc = FrmField::get_option( $this->field, $name . '_desc' ); + + if ( is_string( $desc ) && '' !== trim( $desc ) ) { + return $desc; + } + + $label = $this->sub_fields[ $name ]['label'] ?? ''; + + if ( ! $label ) { + return (string) $this->get_field_column( 'name' ); + } + + /* translators: 1: Field label, 2: Sub field label */ + return sprintf( __( '%1$s %2$s', 'formidable' ), $this->get_field_column( 'name' ), $label ); + } + + /** + * Gets an error message for a single sub field, with the sub field label in place of the + * field label. + * + * @since x.x + * + * @param string $name Sub field name, like 'first' or 'line1'. + * @param string $error Error type, like 'blank'. + * + * @return string + */ + public function get_sub_field_error_msg( $name, $error ) { + $field = is_object( $this->field ) ? clone $this->field : $this->field; + + if ( is_object( $field ) ) { + $field->name = $this->get_sub_field_label( $name ); + } elseif ( is_array( $field ) ) { + $field['name'] = $this->get_sub_field_label( $name ); + } + + return FrmFieldsHelper::get_error_msg( $field, $error ); + } + /** * Gets export headings. * @@ -507,10 +583,17 @@ protected function should_print_hidden_sub_fields() { * @return array */ public function get_inputs_container_attrs() { - return array( + $attrs = array( 'class' => 'frm_combo_inputs_container', 'id' => 'frm_combo_inputs_container_' . $this->field_id, ); + + if ( $this->field && $this->get_field_column( 'required' ) ) { + // JS validation shows this for the whole field when every required sub field is empty. + $attrs['data-reqmsg'] = FrmFieldsHelper::get_error_msg( $this->field, 'blank' ); + } + + return $attrs; } /** diff --git a/css/_single_theme.css.php b/css/_single_theme.css.php index 728d5c98b1..7ac1f34186 100644 --- a/css/_single_theme.css.php +++ b/css/_single_theme.css.php @@ -391,6 +391,16 @@ border-style:var(--border-style-error); } +/* A combo field error only marks the sub fields that failed, not optional or filled ones. */ +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) input:not(:focus), +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) select:not(:focus) { + color:var(--text-color); + background-color:var(--bg-color); + border-color:var(--border-color); + border-width:var(--field-border-width); + border-style:var(--field-border-style); +} + maybeCombineComboFieldErrors( comboContainer, errors ) + ); + vanillaJsObject?.querySelectorAll( 'input,select,textarea' ).forEach( field => { if ( '' === field.value ) { @@ -328,6 +332,12 @@ function frmFrontFormJS() { return; } + const comboContainer = field.closest( '.frm_combo_inputs_container' ); + if ( comboContainer && comboFieldHasFieldError( comboContainer ) ) { + validateComboField( comboContainer, field, addErrors ); + return; + } + if ( hasClass( fieldContainer, 'frm_required_field' ) && ! hasClass( field, 'frm_optional' ) ) { errors = checkRequiredField( field, errors ); } @@ -351,6 +361,185 @@ function frmFrontFormJS() { } } + /** + * Gets the sub field inputs of a combo field, such as name or address, that must be filled in. + * + * @since x.x + * + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @return {HTMLElement[]} Visible inputs that are not optional. + */ + function getRequiredComboSubInputs( comboContainer ) { + return Array.from( comboContainer.querySelectorAll( 'input, select, textarea' ) ).filter( + input => 'hidden' !== input.type && ! hasClass( input, 'frm_optional' ) && null !== input.offsetParent + ); + } + + /** + * Gets the error key of the container for the whole combo field. + * + * @since x.x + * + * @param {HTMLElement} fieldContainer The container of the combo field. + * @return {string} The key, like '12' or '12-5-0' in a repeater. + */ + function getFieldContainerErrorKey( fieldContainer ) { + return fieldContainer.id.replace( /^frm_field_/, '' ).replace( /_container$/, '' ); + } + + /** + * When every required sub field of a combo field is empty, replace the errors for each sub field + * with one error for the whole field. This matches the errors shown after submitting the form. + * + * @since x.x + * + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @param {Object} errors Errors keyed by field container key. Updated in place. + * @return {void} + */ + function maybeCombineComboFieldErrors( comboContainer, errors ) { + const fieldContainer = comboContainer.closest( '.frm_form_field' ); + if ( ! fieldContainer || ! hasClass( fieldContainer, 'frm_required_field' ) ) { + return; + } + + const inputs = getRequiredComboSubInputs( comboContainer ); + const allEmpty = inputs.length && inputs.every( input => Object.keys( checkRequiredField( input, {} ) ).length ); + if ( ! allEmpty ) { + return; + } + + const fieldKey = getFieldContainerErrorKey( fieldContainer ); + let message = getComboFieldRequiredMessage( comboContainer, inputs[ 0 ] ); + + inputs.forEach( input => { + const key = getFieldId( input, true ); + if ( '' === message && errors[ key ] ) { + // Fall back to the sub field message when the field has no message of its own. + message = errors[ key ]; + } + // An empty error still flags the sub field, without repeating the message under it. + // In a repeater the sub fields share the field key, so this is replaced below. + errors[ key ] = ''; + } ); + + errors[ fieldKey ] = message; + } + + /** + * Gets the required message for a whole combo field, wrapped in the custom error HTML when the + * form has any. + * + * @since x.x + * + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @param {HTMLElement} subInput A sub field input of the combo field. + * @return {string} The message, or an empty string if there is none. + */ + function getComboFieldRequiredMessage( comboContainer, subInput ) { + const message = comboContainer.getAttribute( 'data-reqmsg' ); + if ( ! message ) { + return ''; + } + + const subFieldContainer = subInput.closest( '[data-sub-field-name]' ); + const subFieldName = subFieldContainer ? subFieldContainer.getAttribute( 'data-sub-field-name' ) : ''; + // The sub input ID is the field HTML ID plus '_{sub field name}'. The error for the whole + // field is keyed by the field HTML ID without its 'field_' prefix. + let errorKey = subInput.id.replace( /^field_/, '' ); + if ( subFieldName && errorKey.endsWith( `_${ subFieldName }` ) ) { + errorKey = errorKey.slice( 0, -( subFieldName.length + 1 ) ); + } + + return wrapErrorHtml( message, subInput, errorKey ); + } + + /** + * Checks if a combo field is showing an error for the whole field, rather than for its sub fields. + * + * @since x.x + * + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @return {boolean} True if the whole field has an error. + */ + function comboFieldHasFieldError( comboContainer ) { + const fieldContainer = comboContainer.closest( '.frm_form_field' ); + return !! fieldContainer && hasClass( fieldContainer, 'frm_blank_field' ) && null !== fieldContainer.querySelector( ':scope > .frm_error' ); + } + + /** + * Validates every sub field of a combo field together, so an error for the whole field can + * change into errors for the sub fields that are still empty. + * + * @since x.x + * + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @param {HTMLElement} field The sub field input that changed. + * @param {boolean} addErrors Whether to add new errors. + * @return {void} + */ + function validateComboField( comboContainer, field, addErrors ) { + const fieldContainer = comboContainer.closest( '.frm_form_field' ); + let errors = {}; + + if ( hasClass( fieldContainer, 'frm_required_field' ) ) { + getRequiredComboSubInputs( comboContainer ).forEach( input => { + errors = checkRequiredField( input, errors ); + } ); + maybeCombineComboFieldErrors( comboContainer, errors ); + } + + if ( ! ( getFieldId( field, true ) in errors ) ) { + validateFieldValue( field, errors, false ); + } + + const fieldKey = getFieldContainerErrorKey( fieldContainer ); + const hasErrors = Object.keys( errors ).length > 0; + if ( ! addErrors && hasErrors ) { + // JS validation is off, so keep the existing errors until the whole field passes. Only + // unflag the sub field that changed, once it is no longer part of an error. + const subFieldContainer = field.closest( '.frm_form_field' ); + if ( subFieldContainer !== fieldContainer && ! ( fieldKey in errors ) && ! ( getFieldId( field, true ) in errors ) ) { + removeFieldError( subFieldContainer ); + } + return; + } + + removeComboFieldErrors( comboContainer ); + + if ( ! addErrors ) { + return; + } + + const form = fieldContainer.closest( 'form' ); + Object.keys( errors ).forEach( key => { + const container = key === fieldKey ? fieldContainer : form?.querySelector( `#frm_field_${ key }_container` ); + if ( container ) { + addFieldError( container, key, errors ); + } + } ); + } + + /** + * Removes the error for a whole combo field and the errors for each of its sub fields. + * + * @since x.x + * + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @return {void} + */ + function removeComboFieldErrors( comboContainer ) { + const fieldContainer = comboContainer.closest( '.frm_form_field' ); + + comboContainer.querySelectorAll( '.frm_form_field' ).forEach( removeFieldError ); + fieldContainer.classList.remove( 'frm_blank_field', 'has-error' ); + fieldContainer.querySelectorAll( ':scope > .frm_error' ).forEach( errorMessage => { + removeElementFromInputDescribedBy( errorMessage ); + errorMessage.remove(); + } ); + comboContainer.querySelectorAll( '[aria-invalid="true"]' ).forEach( input => input.setAttribute( 'aria-invalid', 'false' ) ); + } + /** * Validates a field value. * @@ -810,11 +999,14 @@ function frmFrontFormJS() { } /** + * @since x.x Added the `keyOverride` parameter. + * * @param {string} msg * @param {HTMLElement} field + * @param {string} keyOverride Optional. The key to use in the error element ID. Defaults to one based on the field. * @return {string} The error HTML to use. */ - function wrapErrorHtml( msg, field ) { + function wrapErrorHtml( msg, field, keyOverride = '' ) { let errorHtml = field.getAttribute( 'data-error-html' ); if ( null === errorHtml ) { return msg; @@ -822,6 +1014,9 @@ function frmFrontFormJS() { errorHtml = errorHtml.replace( /\+/g, '%20' ); msg = decodeURIComponent( errorHtml ).replace( '[error]', msg ); + if ( keyOverride ) { + return msg.replace( '[key]', keyOverride ); + } const fieldId = getFieldId( field, false ); const split = fieldId.split( '-' ); const fieldIdParts = field.id.split( '_' ); @@ -1200,13 +1395,19 @@ function frmFrontFormJS() { } container.classList.add( 'frm_blank_field' ); - const inputs = container.querySelectorAll( 'input, select, textarea' ); - const id = getErrorElementId( key, inputs[ 0 ] ); + const allInputs = container.querySelectorAll( 'input, select, textarea' ); + const id = getErrorElementId( key, allInputs[ 0 ] ); + // An error for a whole combo field, such as an address, does not apply to its optional sub fields. + const inputs = Array.from( allInputs ).filter( + input => ! hasClass( input, 'frm_optional' ) || input.closest( '.frm_form_field' ) === container + ); let describedBy; if ( typeof frmThemeOverride_frmPlaceError === 'function' ) { // eslint-disable-line camelcase frmThemeOverride_frmPlaceError( key, jsErrors ); + } else if ( '' === jsErrors[ key ] ) { + // An empty error flags a combo sub field whose message is shown for the whole field. } else { let errorHtml; if ( jsErrors[ key ].includes( 'create_address_field_type( 'international', 1 ); @@ -388,10 +388,45 @@ public function test_validate_required_flags_empty_sub_fields() { $this->assertArrayNotHasKey( 'field' . $field_id . '-line1', $errors, 'A filled sub-field should not be flagged.' ); $this->assertArrayNotHasKey( 'field' . $field_id . '-line2', $errors, 'The optional line2 sub-field should not be flagged.' ); - $this->assertArrayHasKey( 'field' . $field_id . '-city', $errors, 'An empty required city should be flagged.' ); - $this->assertArrayHasKey( 'field' . $field_id . '-state', $errors, 'An empty required state should be flagged.' ); - $this->assertArrayHasKey( 'field' . $field_id . '-zip', $errors, 'An empty required zip should be flagged.' ); - $this->assertArrayHasKey( 'field' . $field_id, $errors, 'The main field should get the blank message.' ); + $this->assertStringContainsString( 'City', $errors[ 'field' . $field_id . '-city' ], 'An empty required city should be named in its own error.' ); + $this->assertStringContainsString( 'State/Province', $errors[ 'field' . $field_id . '-state' ], 'An empty required state should be named in its own error.' ); + $this->assertStringContainsString( 'Zip/Postal', $errors[ 'field' . $field_id . '-zip' ], 'An empty required zip should be named in its own error.' ); + $this->assertArrayNotHasKey( 'field' . $field_id, $errors, 'The main field should not get an error when only some sub-fields are missing.' ); + } + + /** + * Test validate shows one error for the whole field when every required sub-field is empty. + */ + public function test_validate_required_all_empty_shows_one_field_error() { + $field_type = $this->create_address_field_type( 'international', 1 ); + $field_id = $field_type->get_field()->id; + + $errors = $field_type->validate( + array( + 'errors' => array(), + 'id' => $field_id, + 'value' => array( 'line2' => 'Unit 4' ), + ) + ); + + $this->assertSame( FrmFieldsHelper::get_error_msg( $field_type->get_field(), 'blank' ), $errors[ 'field' . $field_id ], 'The main field should get the blank message.' ); + $this->assertSame( '', $errors[ 'field' . $field_id . '-line1' ], 'Each empty required sub-field should be flagged without a message.' ); + $this->assertSame( '', $errors[ 'field' . $field_id . '-country' ], 'Each empty required sub-field should be flagged without a message.' ); + $this->assertArrayNotHasKey( 'field' . $field_id . '-line2', $errors, 'The optional line2 sub-field should not be flagged.' ); + } + + /** + * Test get_sub_field_label uses the sub-field description, or combines the labels without one. + */ + public function test_get_sub_field_label() { + $field_type = $this->create_address_field_type(); + + $this->assertSame( 'City', $field_type->get_sub_field_label( 'city' ), 'The description should be used when there is one.' ); + $this->assertSame( + $field_type->get_field()->name . ' Line 1', + $field_type->get_sub_field_label( 'line1' ), + 'The field label should be combined with the sub-field label without a description.' + ); } /** diff --git a/tests/phpunit/fields/test_FrmFieldName.php b/tests/phpunit/fields/test_FrmFieldName.php index 7e3652fd1a..98a8465a0a 100644 --- a/tests/phpunit/fields/test_FrmFieldName.php +++ b/tests/phpunit/fields/test_FrmFieldName.php @@ -23,4 +23,49 @@ public function test_get_processed_sub_fields() { $this->assertStringContainsString( 'frm4', $processed_sub_fields['middle']['wrapper_classes'] ); $this->assertStringContainsString( 'frm4', $processed_sub_fields['last']['wrapper_classes'] ); } + + /** + * A required name with only some sub fields missing names each one in its own error, and one + * with every sub field missing gets a single error for the whole field. + * + * @covers FrmFieldCombo::validate + */ + public function test_validate_required_sub_field_errors() { + $field = $this->factory->field->create_and_get( + array( + 'type' => 'name', + 'form_id' => 1, + 'required' => 1, + 'field_options' => array( + 'first_desc' => 'First Name', + 'last_desc' => 'Last Name', + ), + ) + ); + + $name_field = new FrmFieldName( $field ); + $error_key = 'field' . $field->id; + + $errors = $name_field->validate( + array( + 'id' => $field->id, + 'value' => array( 'first' => 'Ann' ), + ) + ); + + $this->assertArrayNotHasKey( $error_key, $errors ); + $this->assertArrayNotHasKey( $error_key . '-first', $errors ); + $this->assertStringContainsString( 'Last Name', $errors[ $error_key . '-last' ] ); + + $errors = $name_field->validate( + array( + 'id' => $field->id, + 'value' => array(), + ) + ); + + $this->assertSame( FrmFieldsHelper::get_error_msg( $field, 'blank' ), $errors[ $error_key ] ); + $this->assertSame( '', $errors[ $error_key . '-first' ] ); + $this->assertSame( '', $errors[ $error_key . '-last' ] ); + } } From f7e68a64beb081b0b62fcce64a33d3a8ceed4a3e Mon Sep 17 00:00:00 2001 From: sanjiwells Date: Mon, 28 Sep 2026 10:18:51 -0600 Subject: [PATCH 2/9] Fix combo sub-field error handling: blur regression + repeater key collision Franky's round-1 review on this PR found two real JS bugs plus a stale minified bundle: - comboFieldHasFieldError() only checked the outer combo wrapper's frm_blank_field flag, which clears as soon as one sub field gets filled - so once a combo field moves from "all blank" to "some blank", blurring the still-empty sub field again silently wiped its own error instead of keeping it flagged. Now also checks for any sub field still individually flagged blank. - getFieldId()'s repeater branch never included the sub-field-name segment, so every sub field of the same combo field inside a repeating section collapsed to the same key - the second checkRequiredField() call overwrote the first sub field's error. Now appends the sub field's data-sub-field-name. - Regenerated js/formidable.min.js via `npm run minimize` (confirmed byte-for-byte reproducible against a fresh run) so the above actually ships - js_suffix() serves the .min bundle whenever SCRIPT_DEBUG is off. Verified red-against-master / green-against-fix with a jsdom harness exercising the real frmFrontForm code against both repro cases from the review. --- js/formidable.js | 33 ++++++++++-- js/formidable.min.js | 117 +++++++++++++++++++++++-------------------- 2 files changed, 92 insertions(+), 58 deletions(-) diff --git a/js/formidable.js b/js/formidable.js index 3e4f1b7864..8084e77475 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -107,6 +107,14 @@ function frmFrontFormJS() { fieldId = `${ fieldId }-${ nameParts[ 1 ].replace( '[', '' ) }`; } else { fieldId = `${ fieldId }-${ nameParts[ 0 ] }-${ nameParts[ 1 ].replace( '[', '' ) }`; + + // Inside a repeating section, every sub field of the same combo field would + // otherwise collapse to this same key, since the sub field name is not part of + // it above. Add it so each sub field still gets its own distinct key/container. + const subFieldContainer = field.closest( '[data-sub-field-name]' ); + if ( subFieldContainer ) { + fieldId += `-${ subFieldContainer.getAttribute( 'data-sub-field-name' ) }`; + } } } @@ -455,16 +463,25 @@ function frmFrontFormJS() { } /** - * Checks if a combo field is showing an error for the whole field, rather than for its sub fields. + * Checks if a combo field, or any of its sub fields, is currently showing a required-field + * error - either the combined error for the whole field, or a split-out error on one sub + * field (see validateComboField). * * @since x.x * * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. - * @return {boolean} True if the whole field has an error. + * @return {boolean} True if the field or one of its sub fields has an error. */ function comboFieldHasFieldError( comboContainer ) { const fieldContainer = comboContainer.closest( '.frm_form_field' ); - return !! fieldContainer && hasClass( fieldContainer, 'frm_blank_field' ) && null !== fieldContainer.querySelector( ':scope > .frm_error' ); + if ( fieldContainer && hasClass( fieldContainer, 'frm_blank_field' ) && null !== fieldContainer.querySelector( ':scope > .frm_error' ) ) { + return true; + } + + // A sub field keeps its own frm_blank_field flag once the whole-field error has already + // been split into per sub field errors. Route through the combo-aware validation in that + // state too, so blurring another still-blank sub field does not wipe its error. + return null !== comboContainer.querySelector( '.frm_form_field.frm_blank_field' ); } /** @@ -511,9 +528,17 @@ function frmFrontFormJS() { return; } + // Resolve each sub field's real container straight from its input, instead of by id + // lookup - the id lookup can miss inside a repeating section, where a sub field + // container's id does not necessarily include the repeating row (see getFieldId()). + const subFieldContainers = {}; + getRequiredComboSubInputs( comboContainer ).forEach( input => { + subFieldContainers[ getFieldId( input, true ) ] = input.closest( '.frm_form_field' ); + } ); + const form = fieldContainer.closest( 'form' ); Object.keys( errors ).forEach( key => { - const container = key === fieldKey ? fieldContainer : form?.querySelector( `#frm_field_${ key }_container` ); + const container = key === fieldKey ? fieldContainer : ( subFieldContainers[ key ] || form?.querySelector( `#frm_field_${ key }_container` ) ); if ( container ) { addFieldError( container, key, errors ); } diff --git a/js/formidable.min.js b/js/formidable.min.js index 264f898265..fa8cb9544f 100644 --- a/js/formidable.min.js +++ b/js/formidable.min.js @@ -1,59 +1,68 @@ function frmFrontFormJS(){let jsErrors=[];const FOCUSABLE_FIELD_SELECTOR='input:not([type="hidden"]), select, textarea, button, [contenteditable="true"], [tabindex]:not([tabindex="-1"])';function triggerCustomEvent(el,eventName,data){if(typeof window.CustomEvent!=="function")return;const event=new CustomEvent(eventName);event.frmData=data;el.dispatchEvent(event)}function getFieldId(field,fullID){let nameParts;let fieldId;let isRepeating=false;let fieldName="";if(field instanceof jQuery)field=field.get(0); fieldName=field.name;if(fieldName===undefined)fieldName="";if(fieldName===""){fieldName=field.getAttribute("data-name");if(fieldName===undefined)fieldName="";if(fieldName!==""&&fieldName)return fieldName;return 0}nameParts=fieldName.replace("item_meta[","").replace("[]","").split("]");if(nameParts.length<1)return 0;nameParts=nameParts.filter(function(n){return n!==""});fieldId=nameParts[0];if(nameParts.length===1)return fieldId;if(nameParts[1]==="[form"||nameParts[1]==="[row_ids")return 0;if(document.querySelector(`input[name="item_meta[${fieldId}][form]"]`)){fieldId= -nameParts[2].replace("[","");isRepeating=true}if("other"===fieldId)if(isRepeating)fieldId=nameParts[3].replace("[","");else fieldId=nameParts[1].replace("[","");if(fullID===true)if(fieldId===nameParts[0])fieldId=`${fieldId}-${nameParts[1].replace("[","")}`;else fieldId=`${fieldId}-${nameParts[0]}-${nameParts[1].replace("[","")}`;return fieldId}function disableSubmitButton($form){const form=$form instanceof jQuery?$form.get(0):$form;if(!form)return;form.querySelectorAll('input[type="submit"], input[type="button"], button[type="submit"], button.frm_save_draft').forEach(button=> -button.disabled=true)}function enableSubmitButton(form){form.querySelectorAll('input[type="submit"], input[type="button"], button[type="submit"]').forEach(button=>button.disabled=false)}function disableSaveDraft($form){const form=$form instanceof jQuery?$form.get(0):$form;if(!form)return;form.querySelectorAll("a.frm_save_draft").forEach(link=>link.style.pointerEvents="none")}function enableSaveDraft($form){const form=$form instanceof jQuery?$form.get(0):$form;if(!form)return;form.querySelectorAll(".frm_save_draft").forEach(saveDraftButton=> -{saveDraftButton.disabled=false;saveDraftButton.style.pointerEvents=""})}function validateForm(object){let errors=[];const vanillaJsObject="function"===typeof object.get?object.get(0):object;vanillaJsObject?.querySelectorAll(".frm_required_field").forEach(requiredField=>{const isVisible=requiredField.offsetParent!==null;if(!isVisible)return;requiredField.querySelectorAll("input, select, textarea").forEach(requiredInput=>{if(hasClass(requiredInput,"frm_optional")||hasClass(requiredInput,"ed_button"))return; -errors=checkRequiredField(requiredInput,errors)})});vanillaJsObject?.querySelectorAll("input,select,textarea").forEach(field=>{if(""===field.value){if("number"===field.type)checkValidity(field,errors);const isConfirmationField=field.name&&0===field.name.indexOf("item_meta[conf_");if(!isConfirmationField)return}validateFieldValue(field,errors,true);checkValidity(field,errors)});if(!hasInvisibleRecaptcha(object))errors=validateRecaptcha(object,errors);return errors}function checkValidity(field,errors){if("object"!== -typeof field.validity||false!==field.validity.valid)return;const fieldID=getFieldId(field,true);if(errors[fieldID]===undefined)errors[fieldID]=getFieldValidationMessage(field,"data-invmsg");if("function"===typeof field.reportValidity)field.reportValidity()}function hasClass(element,targetClass){return element.classList&&element.classList.contains(targetClass)}function maybeValidateChange(field){if(field.type==="url")maybeAddHttpsToUrl(field);const form=field.closest("form");if(!form)return;validateField(field, -hasClass(form,"frm_js_validate"))}function maybeAddHttpsToUrl(field){const url=field.value;const matches=url.match(/^(https?|ftps?|mailto|news|feed|telnet):/);if(field.value!==""&&matches===null)field.value=`https://${url}`}function validateField(field,addErrors=true){let errors;let key;errors=[];const fieldContainer=field.closest(".frm_form_field");if(!fieldContainer)return;if(hasClass(fieldContainer,"frm_required_field")&&!hasClass(field,"frm_optional"))errors=checkRequiredField(field,errors);if(errors.length< -1)validateFieldValue(field,errors,false);const hasErrors=Object.keys(errors).length>0;if(addErrors){removeFieldError(fieldContainer);if(hasErrors)for(key in errors)addFieldError(fieldContainer,key,errors)}else if(!hasErrors)removeFieldError(fieldContainer)}function validateFieldValue(field,errors,onSubmit){if(field.type==="hidden");else if(field.type==="number")checkNumberField(field,errors);else if(field.type==="email")checkEmailField(field,errors,onSubmit);else if(field.type==="password")checkPasswordField(field, -errors,onSubmit);else if(field.type==="url")checkUrlField(field,errors);else if(field.pattern!==null)checkPatternField(field,errors);if("tel"===field.type&&shouldCheckConfirmField(field,onSubmit))confirmField(field,errors);triggerCustomEvent(document,"frm_validate_field_value",{field,errors,onSubmit})}function checkRequiredField(field,errors){let tempVal;let i;let placeholder;let val="";let fieldID="";let fileID=field.getAttribute("data-frmfile");if(field.type==="hidden"&&fileID===null&&!isAppointmentField(field)&& -!isInlineDatepickerField(field))return errors;if(field.type==="checkbox"||field.type==="radio")document.querySelectorAll(`input[name="${field.name}"]`).forEach(function(input){const requiredField=input.closest(".frm_required_field");if(!requiredField)return;const checkedInputs=requiredField.querySelectorAll("input:checked");checkedInputs.forEach(function(checkedInput){val=checkedInput.value})});else if(field.type==="file"||fileID){if(fileID===undefined){fileID=getFieldId(field,true);fileID=fileID.replace("file", -"")}if(errors[fileID]===undefined)val=getFileVals(fileID);fieldID=fileID}else{if(hasClass(field,"frm_pos_none"))return errors;val=jQuery(field).val();if(val===null)val="";else if(typeof val!=="string"){tempVal=val;val="";for(i=0;i()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;if(""!==field.value&&pattern.test(field.value)=== -false)errors[fieldID]=getFieldValidationMessage(field,"data-invmsg");if(shouldCheckConfirmField(field,onSubmit))confirmField(field,errors)}function checkPasswordField(field,errors,onSubmit){if(shouldCheckConfirmField(field,onSubmit))confirmField(field,errors)}function confirmField(field,errors){const fieldID=getFieldId(field,true);const strippedId=field.id.replace("conf_","");const strippedFieldID=fieldID.replace("conf_","");const confirmField=document.getElementById(strippedId.replace("field_","field_conf_")); -if(!confirmField||errors[`conf_${strippedFieldID}`]!==undefined)return;if(fieldID!==strippedFieldID){const firstField=document.getElementById(strippedId);const {value}=firstField;const confirmValue=confirmField.value;if(value!==confirmValue)errors[`conf_${strippedFieldID}`]=getFieldValidationMessage(confirmField,"data-confmsg")}else validateField(confirmField)}function checkNumberField(field,errors){let fieldID;const number=field.value;if(number!==""&&isNaN(number/1)!==false){fieldID=getFieldId(field, -true);if(!(fieldID in errors))errors[fieldID]=getFieldValidationMessage(field,"data-invmsg")}}function checkPatternField(field,errors){let fieldID;const text=field.value;let format=getFieldValidationMessage(field,"pattern");if(format!==""&&text!==""){fieldID=getFieldId(field,true);if(!(fieldID in errors))if("object"===typeof window.frmProForm&&"function"===typeof window.frmProForm.isIntlPhoneInput&&window.frmProForm.isIntlPhoneInput(field)){if(!window.frmProForm.validateIntlPhoneInput(field))errors[fieldID]= -getFieldValidationMessage(field,"data-invmsg")}else{format=new RegExp(`^${format}$`,"i");if(format.test(text)===false)errors[fieldID]=getFieldValidationMessage(field,"data-invmsg")}}}function setSelectPlaceholderColor(){const selects=document.querySelectorAll(".form-field select");const styleElement=document.querySelector(".with_frm_style");const textColorDisabled=styleElement?getComputedStyle(styleElement).getPropertyValue("--text-color-disabled").trim():"";if(!selects.length||!textColorDisabled)return; -const changeSelectColor=function(select){if(select.options[select.selectedIndex]&&hasClass(select.options[select.selectedIndex],"frm-select-placeholder"))select.style.setProperty("color",textColorDisabled,"important");else select.style.color=""};Array.prototype.forEach.call(selects,function(select){changeSelectColor(select);select.addEventListener("change",function(){changeSelectColor(select)})})}function hasInvisibleRecaptcha(object){if(isGoingToPrevPage(object))return false;const form=object instanceof -jQuery?object.get(0):object;if(!form)return false;const recaptcha=form.querySelector('.frm-g-recaptcha[data-size="invisible"], .g-recaptcha[data-size="invisible"]');if(recaptcha){const recaptchaID=recaptcha.dataset.rid;const alreadyChecked=grecaptcha.getResponse(recaptchaID);if(alreadyChecked.length===0)return recaptcha}return false}function executeInvisibleRecaptcha(invisibleRecaptcha){const recaptchaID=invisibleRecaptcha.dataset.rid;grecaptcha.reset(recaptchaID);grecaptcha.execute(recaptchaID)} -function validateRecaptcha(form,errors){const formEl=form instanceof jQuery?form.get(0):form;if(!formEl)return errors;const recaptcha=formEl.querySelector(".frm-g-recaptcha");if(!recaptcha)return errors;const recaptchaID=recaptcha.dataset.rid;let response;try{response=grecaptcha.getResponse(recaptchaID)}catch(e){if(formEl.querySelector('input[name="recaptcha_checked"]'))return errors;response=""}if(response.length===0){const fieldContainer=recaptcha.closest(".frm_form_field");if(fieldContainer?.id){const fieldID= -fieldContainer.id.replace("frm_field_","").replace("_container","");errors[fieldID]=""}}return errors}function getFieldValidationMessage(field,messageType){let msg=field.getAttribute(messageType);if(null===msg)msg="";if(""!==msg&&shouldWrapErrorHtmlAroundMessageType(messageType))msg=wrapErrorHtml(msg,field);return msg}function wrapErrorHtml(msg,field){let errorHtml=field.getAttribute("data-error-html");if(null===errorHtml)return msg;errorHtml=errorHtml.replace(/\+/g,"%20");msg=decodeURIComponent(errorHtml).replace("[error]", -msg);const fieldId=getFieldId(field,false);const split=fieldId.split("-");const fieldIdParts=field.id.split("_");fieldIdParts.shift();split[0]=fieldIdParts.join("_");const errorKey=split.join("-");return msg.replace("[key]",errorKey)}function shouldWrapErrorHtmlAroundMessageType(type){return"pattern"!==type}function shouldJSValidate(object){if("function"===typeof object.get)object=object.get(0);let validate=hasClass(object,"frm_js_validate");if(validate&&typeof frmProForm!=="undefined"&&(frmProForm.savingDraft(object)|| -frmProForm.goingToPreviousPage(object)))validate=false;return validate}function getFormErrors(object,action){const fieldsets=object.querySelectorAll(".frm_form_field");fieldsets.forEach(field=>field.classList.add("frm_doing_ajax"));const data=`${jQuery(object).serialize()}&action=frm_entries_${action}&nonce=${frm_js.nonce}`;const shouldTriggerEvent=object.classList.contains("frm_trigger_event_on_submit");const doRedirect=response=>{jQuery(document).trigger("frmBeforeFormRedirect",[object,response]); -if(!response.openInNewTab){window.location=response.redirect;return}const newTab=window.open(response.redirect,"_blank");if(!newTab&&response.fallbackMsg&&response.content)response.content=response.content.trim().replace(/(<\/div><\/div>)$/,` ${response.fallbackMsg}`)};const success=function(response){const defaultResponse={content:"",errors:{},pass:false};if(response===null)response=defaultResponse;else{response=response.replace(/^\s+|\s+$/g,"");if(response.indexOf("{")===0)response= -JSON.parse(response);else response=defaultResponse}let willRedirect=false;if(response.redirect!==undefined){if(shouldTriggerEvent){triggerCustomEvent(object,"frmSubmitEvent");return}if(response.delay)setTimeout(function(){doRedirect(response)},1E3*response.delay);else doRedirect(response);willRedirect=true}if("string"===typeof response.content&&response.content!==""){if(shouldTriggerEvent){triggerCustomEvent(object,"frmSubmitEvent",{content:response.content});return}removeSubmitLoading(jQuery(object)); -if(frm_js.offset!=-1)frmFrontForm.scrollMsg(jQuery(object),false);const formIdInput=object.querySelector('input[name="form_id"]');const formID=formIdInput?formIdInput.value:"";response.content=response.content.replace(/ frm_pro_form /g," frm_pro_form frm_no_hide ");const replaceContent=jQuery(object).closest(".frm_forms");removeAddedScripts(replaceContent,formID);const delay=maybeSlideOut(replaceContent,response.content);setTimeout(function(){afterFormSubmittedBeforeReplace(object,response);replaceContent.replaceWith(response.content); -addUrlParam(response);if(typeof frmThemeOverride_frmAfterSubmit==="function"){const pageOrderInput=document.querySelector(`input[name="frm_page_order_${formID}"]`);const pageOrder=pageOrderInput?pageOrderInput.value:"";const tempDiv=document.createElement("div");tempDiv.innerHTML=response.content;const formReturnedInput=tempDiv.querySelector('input[name="form_id"]');const formReturned=formReturnedInput?formReturnedInput.value:"";frmThemeOverride_frmAfterSubmit(formReturned,pageOrder,response.content, -object)}afterFormSubmitted(object,response)},delay)}else if(response.errors!==undefined&&Object.keys(response.errors).length){removeSubmitLoading(jQuery(object),"enable");let contSubmit=true;removeAllErrors();let $fieldCont=null;for(const key in response.errors){const fieldContEl=object.querySelector(`#frm_field_${key}_container`);$fieldCont=fieldContEl?jQuery(fieldContEl):jQuery();if($fieldCont.length){if(!$fieldCont.is(":visible")){const inCollapsedSection=$fieldCont.closest(".frm_toggle_container"); -if(inCollapsedSection.length){let frmTrigger=inCollapsedSection.prev();if(!frmTrigger.hasClass("frm_trigger"))frmTrigger=frmTrigger.prev(".frm_trigger");frmTrigger.trigger("click")}}if($fieldCont.is(":visible")){addFieldError($fieldCont,key,response.errors);contSubmit=false}}}object.querySelectorAll(".frm-g-recaptcha, .g-recaptcha, .h-captcha").forEach(function(captchaEl){const recaptchaID=captchaEl.dataset.rid;if(typeof grecaptcha!=="undefined"&&grecaptcha)if(recaptchaID)grecaptcha.reset(recaptchaID); -else grecaptcha.reset();if(typeof hcaptcha!=="undefined"&&hcaptcha)hcaptcha.reset()});if(window.turnstile)object.querySelectorAll(".frm-cf-turnstile").forEach(turnstileField=>turnstileField.dataset.rid&&turnstile.reset(turnstileField.dataset.rid));jQuery(document).trigger("frmFormErrors",[object,response]);fieldsets.forEach(field=>field.classList.remove("frm_doing_ajax"));scrollToFirstField(object);if(contSubmit)object.submit();else{object.insertAdjacentHTML("afterbegin",response.error_message);checkForErrorsAndMaybeSetFocus()}}else if(!willRedirect){showFileLoading(object); -object.submit()}};const error=function(){object.querySelectorAll('input[type="submit"], input[type="button"]').forEach(button=>button.disabled=false);object.submit()};postToAjaxUrl(object,data,success,error)}function postToAjaxUrl(form,data,success,error){let ajaxUrl=frm_js.ajax_url;const action=form.getAttribute("action");if("string"===typeof action&&action.includes("?action=frm_forms_preview"))ajaxUrl=action.split("?action=frm_forms_preview")[0];const ajaxParams={type:"POST",url:ajaxUrl,data,success}; -if("function"===typeof error)ajaxParams.error=error;jQuery.ajax(ajaxParams)}function afterFormSubmitted(object,response){const tempDiv=document.createElement("div");tempDiv.innerHTML=response.content;const formCompleted=tempDiv.querySelector(".frm_message");if(formCompleted)jQuery(document).trigger("frmFormComplete",[object,response]);else jQuery(document).trigger("frmPageChanged",[object,response])}function afterFormSubmittedBeforeReplace(object,response){const tempDiv=document.createElement("div"); -tempDiv.innerHTML=response.content;const formCompleted=tempDiv.querySelector(".frm_message");if(formCompleted)triggerCustomEvent(document,"frmFormCompleteBeforeReplace",{object,response})}function removeAddedScripts(formContainer,formID){const endReplace=document.querySelectorAll(`.frm_end_ajax_${formID}`);if(endReplace.length){formContainer.nextUntil(`.frm_end_ajax_${formID}`).remove();endReplace.forEach(el=>el.remove())}}function maybeSlideOut(oldContent,newContent){let c;let newClass="frm_slideout"; -if(newContent.includes(" frm_slide")){c=oldContent.children();if(newContent.includes(" frm_going_back"))newClass+=" frm_going_back";c.removeClass("frm_going_back");c.addClass(newClass);return 300}return 0}function addUrlParam(response){let url;if(history.pushState&&response.page!==undefined){url=addQueryVar("frm_page",response.page);window.history.pushState({html:response.html},"",`?${url}`)}}function addQueryVar(key,value){key=encodeURI(key);value=encodeURI(value);const kvp=document.location.search.substr(1).split("&"); -let i=kvp.length;while(i--){const x=kvp[i].split("=");if(x[0]==key){x[1]=value;kvp[i]=x.join("=");break}}if(i<0)kvp[kvp.length]=[key,value].join("=");return kvp.join("&")}function getErrorConfigForForm(formEl){const fallback={includeAlertRole:!!frm_js.include_alert_role,focusFirstError:!!frm_js.focus_first_error,focusErrorSummary:false};if(!formEl||!formEl.dataset.frmErrorConfig)return fallback;if(!formEl.frmErrorConfigCache){try{formEl.frmErrorConfigCache=JSON.parse(formEl.dataset.frmErrorConfig)}catch(e){formEl.frmErrorConfigCache=fallback}}return formEl.frmErrorConfigCache}function addFieldError($fieldCont,key,jsErrors){const container=$fieldCont instanceof jQuery?$fieldCont.get(0):$fieldCont;if(!container||container.offsetParent===null)return;container.classList.add("frm_blank_field");const inputs=container.querySelectorAll("input, select, textarea");const id=getErrorElementId(key,inputs[0]);let describedBy; -if(typeof frmThemeOverride_frmPlaceError==="function")frmThemeOverride_frmPlaceError(key,jsErrors);else{let errorHtml;if(jsErrors[key].includes("${jsErrors[key]}`}container.insertAdjacentHTML("beforeend",errorHtml);inputs.forEach(input=>{describedBy=input.getAttribute("aria-describedby");if(!describedBy)describedBy=id;else if(!describedBy.includes(id)&& -!describedBy.includes("frm_error_field_")){const {errorFirst}=input.dataset;if(errorFirst==="0")describedBy=`${describedBy} ${id}`;else describedBy=`${id} ${describedBy}`}input.setAttribute("aria-describedby",describedBy)})}inputs.forEach(input=>{if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","true")}else input.setAttribute("aria-invalid","true")});jQuery(document).trigger("frmAddFieldError", -[jQuery(container),key,jsErrors])}function getErrorElementId(key,input){if(isNaN(key)||!input||!input.id)return`frm_error_field_${key}`;return`frm_error_${input.id}`}function removeFieldError(fieldCont){const container=fieldCont instanceof jQuery?fieldCont.get(0):fieldCont;if(!container)return;const errorMessage=container.querySelector(".frm_error");const input=container.querySelector("input, select, textarea");container.classList.remove("frm_blank_field","has-error");if(input)if("true"===input.getAttribute("aria-invalid"))input.setAttribute("aria-invalid", -"false");else if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","false")}if(errorMessage){removeElementFromInputDescribedBy(errorMessage);errorMessage.remove()}}function removeElementFromInputDescribedBy(el){document.querySelectorAll(`[aria-describedby*="${el.id}"]`).forEach(input=>{let ariaDescribedBy=input.getAttribute("aria-describedby").split(" ");ariaDescribedBy=ariaDescribedBy.filter(value=> -{const trimmedValue=value.trim();return trimmedValue&&trimmedValue!==el.id});if(ariaDescribedBy.length){input.setAttribute("aria-describedby",ariaDescribedBy.join(" "));return}input.removeAttribute("aria-describedby")})}function removeAllErrors(){document.querySelectorAll(".form-field").forEach(field=>{field.classList.remove("frm_blank_field","has-error")});document.querySelectorAll(".form-field .frm_error").forEach(el=>{removeElementFromInputDescribedBy(el);el.remove()});document.querySelectorAll(".frm_error_style").forEach(error=> -error.remove())}function scrollToFirstField(object){if("function"===typeof object.get)object=object.get(0);const field=object.querySelector(".frm_blank_field");if(field)frmFrontForm.scrollMsg(jQuery(field),object,true)}function showSubmitLoading($object){showLoadingIndicator($object);disableSubmitButton($object);disableSaveDraft($object)}function showLoadingIndicator($object){if(!$object.hasClass("frm_loading_form")&&!$object.hasClass("frm_loading_prev")){addLoadingClass($object);$object.trigger("frmStartFormLoading")}} -function addLoadingClass($object){const loadingClass=isGoingToPrevPage($object)?"frm_loading_prev":"frm_loading_form";$object.addClass(loadingClass)}function isGoingToPrevPage($object){return typeof frmProForm!=="undefined"&&frmProForm.goingToPreviousPage($object)}function removeSubmitLoading(_,enable,processesRunning){if(processesRunning>0)return;document.querySelectorAll(".frm_loading_form").forEach(function(form){form.classList.remove("frm_loading_form","frm_loading_prev");jQuery(form).trigger("frmEndFormLoading"); -if(enable==="enable"){enableSubmitButton(form);enableSaveDraft(form)}})}function showFileLoading(object){const loading=document.getElementById("frm_loading");if(!loading)return;const fileInput=object.querySelector("input[type=file]");const fileval=fileInput?fileInput.value:"";if(fileval!=="")setTimeout(function(){jQuery(loading).fadeIn("slow")},2E3)}function confirmClick(){const message=this.dataset.frmconfirm;return confirm(message)}function onHoneypotFieldChange(){const css=window.getComputedStyle(this).boxShadow; -if(css?.match(/inset/))this.remove()}function changeFocusWhenClickComboFieldLabel(){let label;const comboInputsContainer=document.querySelectorAll(".frm_combo_inputs_container");comboInputsContainer.forEach(function(inputsContainer){if(!inputsContainer.closest(".frm_form_field"))return;label=inputsContainer.closest(".frm_form_field").querySelector(".frm_primary_label");if(!label)return;label.addEventListener("click",function(){inputsContainer.querySelector(".frm_form_field:first-child input, .frm_form_field:first-child select, .frm_form_field:first-child textarea").focus()})})} -function maybeFocusOnComboSubField(element){if("FIELDSET"!==element.nodeName)return false;if(!element.querySelector(".frm_combo_inputs_container"))return false;const comboSubfield=element.querySelector('[aria-invalid="true"]');if(comboSubfield){focusInput(comboSubfield);return true}return false}function checkForErrorsAndMaybeSetFocus(){const errors=document.querySelectorAll(".frm_form_field .frm_error");if(!errors.length)return;const formContainer=errors[0].closest(".frm-show-form");const config=getErrorConfigForForm(formContainer);if(config.focusErrorSummary){const summary=formContainer?formContainer.querySelector("[data-frm-error-summary]"):null;if(summary){summary.focus();return}}if(!config.focusFirstError)return;let element=errors[0];let timeoutCallback; -do{element=element.previousSibling;if(["input","select","textarea"].includes(element.nodeName.toLowerCase())){focusInput(element);break}if(maybeFocusOnComboSubField(element))break;if(element.classList!==undefined){if(element.classList.contains("html-active"))timeoutCallback=function(){const textarea=element.querySelector("textarea");if(null!==textarea)textarea.focus()};else if(element.classList.contains("tmce-active"))timeoutCallback=function(){tinyMCE.activeEditor.focus()};else if(element.classList.contains("frm_opt_container")){const firstInput= -element.querySelector("input");if(firstInput){focusInput(firstInput);break}}if("function"===typeof timeoutCallback){setTimeout(timeoutCallback,0);break}}}while(element.previousSibling)}function focusInput(input){if(input.offsetParent!==null)input.focus();else triggerCustomEvent(document,"frmMaybeDelayFocus",{input})}function focusFieldFromErrorLink(event){const href=this.getAttribute("href");if(!href||!href.startsWith("#"))return;const container=document.getElementById(href.substring(1));if(!container)return; -event.preventDefault();container.scrollIntoView({behavior:"smooth",block:"center"});const input=getFocusableInputInField(container);if(input){focusInput(input);return}container.setAttribute("tabindex","-1");focusInput(container)}function getFocusableInputInField(container){const inputs=Array.from(container.querySelectorAll(FOCUSABLE_FIELD_SELECTOR)).filter(inputCanTakeFocus);if(!inputs.length)return null;const invalidInput=inputs.find(input=>"true"===input.getAttribute("aria-invalid"));if(invalidInput)return invalidInput; -return inputs.find(fieldInputIsEmpty)||inputs[0]}function fieldInputIsEmpty(input){if("BUTTON"===input.nodeName||["button","checkbox","file","radio","submit"].includes(input.type))return false;return""===String(input.value||"").trim()}function inputCanTakeFocus(input){if(input.disabled||"hidden"===input.type)return false;const rect=input.getBoundingClientRect();if(!rect.width&&!rect.height)return false;return"hidden"!==getComputedStyle(input).visibility}function documentOn(event,selector,handler, -options){if(options===undefined)options=false;document.addEventListener(event,function(e){let target;for(target=e.target;target&&target!=this;target=target.parentNode)if(target.matches&&target.matches(selector)){handler.call(target,e);break}},options)}function initFloatingLabels(){const selector=".frm-show-form .frm_inside_container input, .frm-show-form .frm_inside_container select, .frm-show-form .frm_inside_container textarea";const floatClass="frm_label_float_top";const checkFloatLabel=function(input){const container= -input.closest(".frm_inside_container");if(!container)return;const shouldFloatTop=input.value||document.activeElement===input;container.classList.toggle(floatClass,shouldFloatTop);if("SELECT"===input.tagName){const firstOpt=input.querySelector("option:first-child");if(shouldFloatTop){if(firstOpt.hasAttribute("data-label")){firstOpt.textContent=firstOpt.getAttribute("data-label");firstOpt.removeAttribute("data-label")}}else if(firstOpt.textContent){firstOpt.setAttribute("data-label",firstOpt.textContent); -firstOpt.textContent=""}}};const checkDropdownLabel=function(){document.querySelectorAll(`.frm-show-form .frm_inside_container:not(.${floatClass}) select`).forEach(function(input){const firstOpt=input.querySelector("option:first-child");if(firstOpt.textContent){firstOpt.setAttribute("data-label",firstOpt.textContent);firstOpt.textContent=""}})};["focus","blur","change"].forEach(function(eventName){documentOn(eventName,selector,function(event){checkFloatLabel(event.target)},true)});const runOnLoad= -function(firstLoad){if(firstLoad&&document.activeElement&&["INPUT","SELECT","TEXTAREA"].includes(document.activeElement.tagName))checkFloatLabel(document.activeElement);else if(firstLoad)document.querySelectorAll(".frm_inside_container").forEach(function(container){const input=container.querySelector("input, select, textarea");if(input&&""!==input.value)checkFloatLabel(input)});checkDropdownLabel();calcProductsTotal()};runOnLoad(true);jQuery(document).on("frmPageChanged",function(event){runOnLoad()}); +nameParts[2].replace("[","");isRepeating=true}if("other"===fieldId)if(isRepeating)fieldId=nameParts[3].replace("[","");else fieldId=nameParts[1].replace("[","");if(fullID===true)if(fieldId===nameParts[0])fieldId=`${fieldId}-${nameParts[1].replace("[","")}`;else{fieldId=`${fieldId}-${nameParts[0]}-${nameParts[1].replace("[","")}`;const subFieldContainer=field.closest("[data-sub-field-name]");if(subFieldContainer)fieldId+=`-${subFieldContainer.getAttribute("data-sub-field-name")}`}return fieldId}function disableSubmitButton($form){const form= +$form instanceof jQuery?$form.get(0):$form;if(!form)return;form.querySelectorAll('input[type="submit"], input[type="button"], button[type="submit"], button.frm_save_draft').forEach(button=>button.disabled=true)}function enableSubmitButton(form){form.querySelectorAll('input[type="submit"], input[type="button"], button[type="submit"]').forEach(button=>button.disabled=false)}function disableSaveDraft($form){const form=$form instanceof jQuery?$form.get(0):$form;if(!form)return;form.querySelectorAll("a.frm_save_draft").forEach(link=> +link.style.pointerEvents="none")}function enableSaveDraft($form){const form=$form instanceof jQuery?$form.get(0):$form;if(!form)return;form.querySelectorAll(".frm_save_draft").forEach(saveDraftButton=>{saveDraftButton.disabled=false;saveDraftButton.style.pointerEvents=""})}function validateForm(object){let errors=[];const vanillaJsObject="function"===typeof object.get?object.get(0):object;vanillaJsObject?.querySelectorAll(".frm_required_field").forEach(requiredField=>{const isVisible=requiredField.offsetParent!== +null;if(!isVisible)return;requiredField.querySelectorAll("input, select, textarea").forEach(requiredInput=>{if(hasClass(requiredInput,"frm_optional")||hasClass(requiredInput,"ed_button"))return;errors=checkRequiredField(requiredInput,errors)})});vanillaJsObject?.querySelectorAll(".frm_required_field .frm_combo_inputs_container").forEach(comboContainer=>maybeCombineComboFieldErrors(comboContainer,errors));vanillaJsObject?.querySelectorAll("input,select,textarea").forEach(field=>{if(""===field.value){if("number"=== +field.type)checkValidity(field,errors);const isConfirmationField=field.name&&0===field.name.indexOf("item_meta[conf_");if(!isConfirmationField)return}validateFieldValue(field,errors,true);checkValidity(field,errors)});if(!hasInvisibleRecaptcha(object))errors=validateRecaptcha(object,errors);return errors}function checkValidity(field,errors){if("object"!==typeof field.validity||false!==field.validity.valid)return;const fieldID=getFieldId(field,true);if(errors[fieldID]===undefined)errors[fieldID]=getFieldValidationMessage(field, +"data-invmsg");if("function"===typeof field.reportValidity)field.reportValidity()}function hasClass(element,targetClass){return element.classList&&element.classList.contains(targetClass)}function maybeValidateChange(field){if(field.type==="url")maybeAddHttpsToUrl(field);const form=field.closest("form");if(!form)return;validateField(field,hasClass(form,"frm_js_validate"))}function maybeAddHttpsToUrl(field){const url=field.value;const matches=url.match(/^(https?|ftps?|mailto|news|feed|telnet):/);if(field.value!== +""&&matches===null)field.value=`https://${url}`}function validateField(field,addErrors=true){let errors;let key;errors=[];const fieldContainer=field.closest(".frm_form_field");if(!fieldContainer)return;const comboContainer=field.closest(".frm_combo_inputs_container");if(comboContainer&&comboFieldHasFieldError(comboContainer)){validateComboField(comboContainer,field,addErrors);return}if(hasClass(fieldContainer,"frm_required_field")&&!hasClass(field,"frm_optional"))errors=checkRequiredField(field,errors); +if(errors.length<1)validateFieldValue(field,errors,false);const hasErrors=Object.keys(errors).length>0;if(addErrors){removeFieldError(fieldContainer);if(hasErrors)for(key in errors)addFieldError(fieldContainer,key,errors)}else if(!hasErrors)removeFieldError(fieldContainer)}function getRequiredComboSubInputs(comboContainer){return Array.from(comboContainer.querySelectorAll("input, select, textarea")).filter(input=>"hidden"!==input.type&&!hasClass(input,"frm_optional")&&null!==input.offsetParent)}function getFieldContainerErrorKey(fieldContainer){return fieldContainer.id.replace(/^frm_field_/, +"").replace(/_container$/,"")}function maybeCombineComboFieldErrors(comboContainer,errors){const fieldContainer=comboContainer.closest(".frm_form_field");if(!fieldContainer||!hasClass(fieldContainer,"frm_required_field"))return;const inputs=getRequiredComboSubInputs(comboContainer);const allEmpty=inputs.length&&inputs.every(input=>Object.keys(checkRequiredField(input,{})).length);if(!allEmpty)return;const fieldKey=getFieldContainerErrorKey(fieldContainer);let message=getComboFieldRequiredMessage(comboContainer, +inputs[0]);inputs.forEach(input=>{const key=getFieldId(input,true);if(""===message&&errors[key])message=errors[key];errors[key]=""});errors[fieldKey]=message}function getComboFieldRequiredMessage(comboContainer,subInput){const message=comboContainer.getAttribute("data-reqmsg");if(!message)return"";const subFieldContainer=subInput.closest("[data-sub-field-name]");const subFieldName=subFieldContainer?subFieldContainer.getAttribute("data-sub-field-name"):"";let errorKey=subInput.id.replace(/^field_/, +"");if(subFieldName&&errorKey.endsWith(`_${subFieldName}`))errorKey=errorKey.slice(0,-(subFieldName.length+1));return wrapErrorHtml(message,subInput,errorKey)}function comboFieldHasFieldError(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");if(fieldContainer&&hasClass(fieldContainer,"frm_blank_field")&&null!==fieldContainer.querySelector(":scope > .frm_error"))return true;return null!==comboContainer.querySelector(".frm_form_field.frm_blank_field")}function validateComboField(comboContainer, +field,addErrors){const fieldContainer=comboContainer.closest(".frm_form_field");let errors={};if(hasClass(fieldContainer,"frm_required_field")){getRequiredComboSubInputs(comboContainer).forEach(input=>{errors=checkRequiredField(input,errors)});maybeCombineComboFieldErrors(comboContainer,errors)}if(!(getFieldId(field,true)in errors))validateFieldValue(field,errors,false);const fieldKey=getFieldContainerErrorKey(fieldContainer);const hasErrors=Object.keys(errors).length>0;if(!addErrors&&hasErrors){const subFieldContainer= +field.closest(".frm_form_field");if(subFieldContainer!==fieldContainer&&!(fieldKey in errors)&&!(getFieldId(field,true)in errors))removeFieldError(subFieldContainer);return}removeComboFieldErrors(comboContainer);if(!addErrors)return;const subFieldContainers={};getRequiredComboSubInputs(comboContainer).forEach(input=>{subFieldContainers[getFieldId(input,true)]=input.closest(".frm_form_field")});const form=fieldContainer.closest("form");Object.keys(errors).forEach(key=>{const container=key===fieldKey? +fieldContainer:subFieldContainers[key]||form?.querySelector(`#frm_field_${key}_container`);if(container)addFieldError(container,key,errors)})}function removeComboFieldErrors(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");comboContainer.querySelectorAll(".frm_form_field").forEach(removeFieldError);fieldContainer.classList.remove("frm_blank_field","has-error");fieldContainer.querySelectorAll(":scope > .frm_error").forEach(errorMessage=>{removeElementFromInputDescribedBy(errorMessage); +errorMessage.remove()});comboContainer.querySelectorAll('[aria-invalid="true"]').forEach(input=>input.setAttribute("aria-invalid","false"))}function validateFieldValue(field,errors,onSubmit){if(field.type==="hidden");else if(field.type==="number")checkNumberField(field,errors);else if(field.type==="email")checkEmailField(field,errors,onSubmit);else if(field.type==="password")checkPasswordField(field,errors,onSubmit);else if(field.type==="url")checkUrlField(field,errors);else if(field.pattern!==null)checkPatternField(field, +errors);if("tel"===field.type&&shouldCheckConfirmField(field,onSubmit))confirmField(field,errors);triggerCustomEvent(document,"frm_validate_field_value",{field,errors,onSubmit})}function checkRequiredField(field,errors){let tempVal;let i;let placeholder;let val="";let fieldID="";let fileID=field.getAttribute("data-frmfile");if(field.type==="hidden"&&fileID===null&&!isAppointmentField(field)&&!isInlineDatepickerField(field))return errors;if(field.type==="checkbox"||field.type==="radio")document.querySelectorAll(`input[name="${field.name}"]`).forEach(function(input){const requiredField= +input.closest(".frm_required_field");if(!requiredField)return;const checkedInputs=requiredField.querySelectorAll("input:checked");checkedInputs.forEach(function(checkedInput){val=checkedInput.value})});else if(field.type==="file"||fileID){if(fileID===undefined){fileID=getFieldId(field,true);fileID=fileID.replace("file","")}if(errors[fileID]===undefined)val=getFileVals(fileID);fieldID=fileID}else{if(hasClass(field,"frm_pos_none"))return errors;val=jQuery(field).val();if(val===null)val="";else if(typeof val!== +"string"){tempVal=val;val="";for(i=0;i()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;if(""!==field.value&&pattern.test(field.value)===false)errors[fieldID]=getFieldValidationMessage(field,"data-invmsg");if(shouldCheckConfirmField(field,onSubmit))confirmField(field,errors)}function checkPasswordField(field,errors,onSubmit){if(shouldCheckConfirmField(field,onSubmit))confirmField(field, +errors)}function confirmField(field,errors){const fieldID=getFieldId(field,true);const strippedId=field.id.replace("conf_","");const strippedFieldID=fieldID.replace("conf_","");const confirmField=document.getElementById(strippedId.replace("field_","field_conf_"));if(!confirmField||errors[`conf_${strippedFieldID}`]!==undefined)return;if(fieldID!==strippedFieldID){const firstField=document.getElementById(strippedId);const {value}=firstField;const confirmValue=confirmField.value;if(value!==confirmValue)errors[`conf_${strippedFieldID}`]= +getFieldValidationMessage(confirmField,"data-confmsg")}else validateField(confirmField)}function checkNumberField(field,errors){let fieldID;const number=field.value;if(number!==""&&isNaN(number/1)!==false){fieldID=getFieldId(field,true);if(!(fieldID in errors))errors[fieldID]=getFieldValidationMessage(field,"data-invmsg")}}function checkPatternField(field,errors){let fieldID;const text=field.value;let format=getFieldValidationMessage(field,"pattern");if(format!==""&&text!==""){fieldID=getFieldId(field, +true);if(!(fieldID in errors))if("object"===typeof window.frmProForm&&"function"===typeof window.frmProForm.isIntlPhoneInput&&window.frmProForm.isIntlPhoneInput(field)){if(!window.frmProForm.validateIntlPhoneInput(field))errors[fieldID]=getFieldValidationMessage(field,"data-invmsg")}else{format=new RegExp(`^${format}$`,"i");if(format.test(text)===false)errors[fieldID]=getFieldValidationMessage(field,"data-invmsg")}}}function setSelectPlaceholderColor(){const selects=document.querySelectorAll(".form-field select"); +const styleElement=document.querySelector(".with_frm_style");const textColorDisabled=styleElement?getComputedStyle(styleElement).getPropertyValue("--text-color-disabled").trim():"";if(!selects.length||!textColorDisabled)return;const changeSelectColor=function(select){if(select.options[select.selectedIndex]&&hasClass(select.options[select.selectedIndex],"frm-select-placeholder"))select.style.setProperty("color",textColorDisabled,"important");else select.style.color=""};Array.prototype.forEach.call(selects, +function(select){changeSelectColor(select);select.addEventListener("change",function(){changeSelectColor(select)})})}function hasInvisibleRecaptcha(object){if(isGoingToPrevPage(object))return false;const form=object instanceof jQuery?object.get(0):object;if(!form)return false;const recaptcha=form.querySelector('.frm-g-recaptcha[data-size="invisible"], .g-recaptcha[data-size="invisible"]');if(recaptcha){const recaptchaID=recaptcha.dataset.rid;const alreadyChecked=grecaptcha.getResponse(recaptchaID); +if(alreadyChecked.length===0)return recaptcha}return false}function executeInvisibleRecaptcha(invisibleRecaptcha){const recaptchaID=invisibleRecaptcha.dataset.rid;grecaptcha.reset(recaptchaID);grecaptcha.execute(recaptchaID)}function validateRecaptcha(form,errors){const formEl=form instanceof jQuery?form.get(0):form;if(!formEl)return errors;const recaptcha=formEl.querySelector(".frm-g-recaptcha");if(!recaptcha)return errors;const recaptchaID=recaptcha.dataset.rid;let response;try{response=grecaptcha.getResponse(recaptchaID)}catch(e){if(formEl.querySelector('input[name="recaptcha_checked"]'))return errors; +response=""}if(response.length===0){const fieldContainer=recaptcha.closest(".frm_form_field");if(fieldContainer?.id){const fieldID=fieldContainer.id.replace("frm_field_","").replace("_container","");errors[fieldID]=""}}return errors}function getFieldValidationMessage(field,messageType){let msg=field.getAttribute(messageType);if(null===msg)msg="";if(""!==msg&&shouldWrapErrorHtmlAroundMessageType(messageType))msg=wrapErrorHtml(msg,field);return msg}function wrapErrorHtml(msg,field,keyOverride=""){let errorHtml= +field.getAttribute("data-error-html");if(null===errorHtml)return msg;errorHtml=errorHtml.replace(/\+/g,"%20");msg=decodeURIComponent(errorHtml).replace("[error]",msg);if(keyOverride)return msg.replace("[key]",keyOverride);const fieldId=getFieldId(field,false);const split=fieldId.split("-");const fieldIdParts=field.id.split("_");fieldIdParts.shift();split[0]=fieldIdParts.join("_");const errorKey=split.join("-");return msg.replace("[key]",errorKey)}function shouldWrapErrorHtmlAroundMessageType(type){return"pattern"!== +type}function shouldJSValidate(object){if("function"===typeof object.get)object=object.get(0);let validate=hasClass(object,"frm_js_validate");if(validate&&typeof frmProForm!=="undefined"&&(frmProForm.savingDraft(object)||frmProForm.goingToPreviousPage(object)))validate=false;return validate}function getFormErrors(object,action){const fieldsets=object.querySelectorAll(".frm_form_field");fieldsets.forEach(field=>field.classList.add("frm_doing_ajax"));const data=`${jQuery(object).serialize()}&action=frm_entries_${action}&nonce=${frm_js.nonce}`; +const shouldTriggerEvent=object.classList.contains("frm_trigger_event_on_submit");const doRedirect=response=>{jQuery(document).trigger("frmBeforeFormRedirect",[object,response]);if(!response.openInNewTab){window.location=response.redirect;return}const newTab=window.open(response.redirect,"_blank");if(!newTab&&response.fallbackMsg&&response.content)response.content=response.content.trim().replace(/(<\/div><\/div>)$/,` ${response.fallbackMsg}`)};const success=function(response){const defaultResponse= +{content:"",errors:{},pass:false};if(response===null)response=defaultResponse;else{response=response.replace(/^\s+|\s+$/g,"");if(response.indexOf("{")===0)response=JSON.parse(response);else response=defaultResponse}let willRedirect=false;if(response.redirect!==undefined){if(shouldTriggerEvent){triggerCustomEvent(object,"frmSubmitEvent");return}if(response.delay)setTimeout(function(){doRedirect(response)},1E3*response.delay);else doRedirect(response);willRedirect=true}if("string"===typeof response.content&& +response.content!==""){if(shouldTriggerEvent){triggerCustomEvent(object,"frmSubmitEvent",{content:response.content});return}removeSubmitLoading(jQuery(object));if(frm_js.offset!=-1)frmFrontForm.scrollMsg(jQuery(object),false);const formIdInput=object.querySelector('input[name="form_id"]');const formID=formIdInput?formIdInput.value:"";response.content=response.content.replace(/ frm_pro_form /g," frm_pro_form frm_no_hide ");const replaceContent=jQuery(object).closest(".frm_forms");removeAddedScripts(replaceContent, +formID);const delay=maybeSlideOut(replaceContent,response.content);setTimeout(function(){afterFormSubmittedBeforeReplace(object,response);replaceContent.replaceWith(response.content);addUrlParam(response);if(typeof frmThemeOverride_frmAfterSubmit==="function"){const pageOrderInput=document.querySelector(`input[name="frm_page_order_${formID}"]`);const pageOrder=pageOrderInput?pageOrderInput.value:"";const tempDiv=document.createElement("div");tempDiv.innerHTML=response.content;const formReturnedInput= +tempDiv.querySelector('input[name="form_id"]');const formReturned=formReturnedInput?formReturnedInput.value:"";frmThemeOverride_frmAfterSubmit(formReturned,pageOrder,response.content,object)}afterFormSubmitted(object,response)},delay)}else if(response.errors!==undefined&&Object.keys(response.errors).length){removeSubmitLoading(jQuery(object),"enable");let contSubmit=true;removeAllErrors();let $fieldCont=null;for(const key in response.errors){const fieldContEl=object.querySelector(`#frm_field_${key}_container`); +$fieldCont=fieldContEl?jQuery(fieldContEl):jQuery();if($fieldCont.length){if(!$fieldCont.is(":visible")){const inCollapsedSection=$fieldCont.closest(".frm_toggle_container");if(inCollapsedSection.length){let frmTrigger=inCollapsedSection.prev();if(!frmTrigger.hasClass("frm_trigger"))frmTrigger=frmTrigger.prev(".frm_trigger");frmTrigger.trigger("click")}}if($fieldCont.is(":visible")){addFieldError($fieldCont,key,response.errors);contSubmit=false}}}object.querySelectorAll(".frm-g-recaptcha, .g-recaptcha, .h-captcha").forEach(function(captchaEl){const recaptchaID= +captchaEl.dataset.rid;if(typeof grecaptcha!=="undefined"&&grecaptcha)if(recaptchaID)grecaptcha.reset(recaptchaID);else grecaptcha.reset();if(typeof hcaptcha!=="undefined"&&hcaptcha)hcaptcha.reset()});if(window.turnstile)object.querySelectorAll(".frm-cf-turnstile").forEach(turnstileField=>turnstileField.dataset.rid&&turnstile.reset(turnstileField.dataset.rid));jQuery(document).trigger("frmFormErrors",[object,response]);fieldsets.forEach(field=>field.classList.remove("frm_doing_ajax"));scrollToFirstField(object); +if(contSubmit)object.submit();else{object.insertAdjacentHTML("afterbegin",response.error_message);checkForErrorsAndMaybeSetFocus()}}else if(!willRedirect){showFileLoading(object);object.submit()}};const error=function(){object.querySelectorAll('input[type="submit"], input[type="button"]').forEach(button=>button.disabled=false);object.submit()};postToAjaxUrl(object,data,success,error)}function postToAjaxUrl(form,data,success,error){let ajaxUrl=frm_js.ajax_url;const action=form.getAttribute("action"); +if("string"===typeof action&&action.includes("?action=frm_forms_preview"))ajaxUrl=action.split("?action=frm_forms_preview")[0];const ajaxParams={type:"POST",url:ajaxUrl,data,success};if("function"===typeof error)ajaxParams.error=error;jQuery.ajax(ajaxParams)}function afterFormSubmitted(object,response){const tempDiv=document.createElement("div");tempDiv.innerHTML=response.content;const formCompleted=tempDiv.querySelector(".frm_message");if(formCompleted)jQuery(document).trigger("frmFormComplete", +[object,response]);else jQuery(document).trigger("frmPageChanged",[object,response])}function afterFormSubmittedBeforeReplace(object,response){const tempDiv=document.createElement("div");tempDiv.innerHTML=response.content;const formCompleted=tempDiv.querySelector(".frm_message");if(formCompleted)triggerCustomEvent(document,"frmFormCompleteBeforeReplace",{object,response})}function removeAddedScripts(formContainer,formID){const endReplace=document.querySelectorAll(`.frm_end_ajax_${formID}`);if(endReplace.length){formContainer.nextUntil(`.frm_end_ajax_${formID}`).remove(); +endReplace.forEach(el=>el.remove())}}function maybeSlideOut(oldContent,newContent){let c;let newClass="frm_slideout";if(newContent.includes(" frm_slide")){c=oldContent.children();if(newContent.includes(" frm_going_back"))newClass+=" frm_going_back";c.removeClass("frm_going_back");c.addClass(newClass);return 300}return 0}function addUrlParam(response){let url;if(history.pushState&&response.page!==undefined){url=addQueryVar("frm_page",response.page);window.history.pushState({html:response.html},"", +`?${url}`)}}function addQueryVar(key,value){key=encodeURI(key);value=encodeURI(value);const kvp=document.location.search.substr(1).split("&");let i=kvp.length;while(i--){const x=kvp[i].split("=");if(x[0]==key){x[1]=value;kvp[i]=x.join("=");break}}if(i<0)kvp[kvp.length]=[key,value].join("=");return kvp.join("&")}function getErrorConfigForForm(formEl){const fallback={includeAlertRole:!!frm_js.include_alert_role,focusFirstError:!!frm_js.focus_first_error,focusErrorSummary:false};if(!formEl||!formEl.dataset.frmErrorConfig)return fallback; +if(!formEl.frmErrorConfigCache)try{formEl.frmErrorConfigCache=JSON.parse(formEl.dataset.frmErrorConfig)}catch(e){formEl.frmErrorConfigCache=fallback}return formEl.frmErrorConfigCache}function addFieldError($fieldCont,key,jsErrors){const container=$fieldCont instanceof jQuery?$fieldCont.get(0):$fieldCont;if(!container||container.offsetParent===null)return;container.classList.add("frm_blank_field");const allInputs=container.querySelectorAll("input, select, textarea");const id=getErrorElementId(key, +allInputs[0]);const inputs=Array.from(allInputs).filter(input=>!hasClass(input,"frm_optional")||input.closest(".frm_form_field")===container);let describedBy;if(typeof frmThemeOverride_frmPlaceError==="function")frmThemeOverride_frmPlaceError(key,jsErrors);else if(""===jsErrors[key]);else{let errorHtml;if(jsErrors[key].includes("${jsErrors[key]}`}container.insertAdjacentHTML("beforeend",errorHtml);inputs.forEach(input=>{describedBy=input.getAttribute("aria-describedby");if(!describedBy)describedBy=id;else if(!describedBy.includes(id)&&!describedBy.includes("frm_error_field_")){const {errorFirst}=input.dataset;if(errorFirst==="0")describedBy=`${describedBy} ${id}`;else describedBy=`${id} ${describedBy}`}input.setAttribute("aria-describedby",describedBy)})}inputs.forEach(input=> +{if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","true")}else input.setAttribute("aria-invalid","true")});jQuery(document).trigger("frmAddFieldError",[jQuery(container),key,jsErrors])}function getErrorElementId(key,input){if(isNaN(key)||!input||!input.id)return`frm_error_field_${key}`;return`frm_error_${input.id}`}function removeFieldError(fieldCont){const container=fieldCont instanceof jQuery? +fieldCont.get(0):fieldCont;if(!container)return;const errorMessage=container.querySelector(".frm_error");const input=container.querySelector("input, select, textarea");container.classList.remove("frm_blank_field","has-error");if(input)if("true"===input.getAttribute("aria-invalid"))input.setAttribute("aria-invalid","false");else if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","false")}if(errorMessage){removeElementFromInputDescribedBy(errorMessage); +errorMessage.remove()}}function removeElementFromInputDescribedBy(el){document.querySelectorAll(`[aria-describedby*="${el.id}"]`).forEach(input=>{let ariaDescribedBy=input.getAttribute("aria-describedby").split(" ");ariaDescribedBy=ariaDescribedBy.filter(value=>{const trimmedValue=value.trim();return trimmedValue&&trimmedValue!==el.id});if(ariaDescribedBy.length){input.setAttribute("aria-describedby",ariaDescribedBy.join(" "));return}input.removeAttribute("aria-describedby")})}function removeAllErrors(){document.querySelectorAll(".form-field").forEach(field=> +{field.classList.remove("frm_blank_field","has-error")});document.querySelectorAll(".form-field .frm_error").forEach(el=>{removeElementFromInputDescribedBy(el);el.remove()});document.querySelectorAll(".frm_error_style").forEach(error=>error.remove())}function scrollToFirstField(object){if("function"===typeof object.get)object=object.get(0);const field=object.querySelector(".frm_blank_field");if(field)frmFrontForm.scrollMsg(jQuery(field),object,true)}function showSubmitLoading($object){showLoadingIndicator($object); +disableSubmitButton($object);disableSaveDraft($object)}function showLoadingIndicator($object){if(!$object.hasClass("frm_loading_form")&&!$object.hasClass("frm_loading_prev")){addLoadingClass($object);$object.trigger("frmStartFormLoading")}}function addLoadingClass($object){const loadingClass=isGoingToPrevPage($object)?"frm_loading_prev":"frm_loading_form";$object.addClass(loadingClass)}function isGoingToPrevPage($object){return typeof frmProForm!=="undefined"&&frmProForm.goingToPreviousPage($object)} +function removeSubmitLoading(_,enable,processesRunning){if(processesRunning>0)return;document.querySelectorAll(".frm_loading_form").forEach(function(form){form.classList.remove("frm_loading_form","frm_loading_prev");jQuery(form).trigger("frmEndFormLoading");if(enable==="enable"){enableSubmitButton(form);enableSaveDraft(form)}})}function showFileLoading(object){const loading=document.getElementById("frm_loading");if(!loading)return;const fileInput=object.querySelector("input[type=file]");const fileval= +fileInput?fileInput.value:"";if(fileval!=="")setTimeout(function(){jQuery(loading).fadeIn("slow")},2E3)}function confirmClick(){const message=this.dataset.frmconfirm;return confirm(message)}function onHoneypotFieldChange(){const css=window.getComputedStyle(this).boxShadow;if(css?.match(/inset/))this.remove()}function changeFocusWhenClickComboFieldLabel(){let label;const comboInputsContainer=document.querySelectorAll(".frm_combo_inputs_container");comboInputsContainer.forEach(function(inputsContainer){if(!inputsContainer.closest(".frm_form_field"))return; +label=inputsContainer.closest(".frm_form_field").querySelector(".frm_primary_label");if(!label)return;label.addEventListener("click",function(){inputsContainer.querySelector(".frm_form_field:first-child input, .frm_form_field:first-child select, .frm_form_field:first-child textarea").focus()})})}function maybeFocusOnComboSubField(element){if("FIELDSET"!==element.nodeName)return false;if(!element.querySelector(".frm_combo_inputs_container"))return false;const comboSubfield=element.querySelector('[aria-invalid="true"]'); +if(comboSubfield){focusInput(comboSubfield);return true}return false}function checkForErrorsAndMaybeSetFocus(){const errors=document.querySelectorAll(".frm_form_field .frm_error");if(!errors.length)return;const formContainer=errors[0].closest(".frm-show-form");const config=getErrorConfigForForm(formContainer);if(config.focusErrorSummary){const summary=formContainer?formContainer.querySelector("[data-frm-error-summary]"):null;if(summary){summary.focus();return}}if(!config.focusFirstError)return;let element= +errors[0];let timeoutCallback;do{element=element.previousSibling;if(["input","select","textarea"].includes(element.nodeName.toLowerCase())){focusInput(element);break}if(maybeFocusOnComboSubField(element))break;if(element.classList!==undefined){if(element.classList.contains("html-active"))timeoutCallback=function(){const textarea=element.querySelector("textarea");if(null!==textarea)textarea.focus()};else if(element.classList.contains("tmce-active"))timeoutCallback=function(){tinyMCE.activeEditor.focus()}; +else if(element.classList.contains("frm_opt_container")){const firstInput=element.querySelector("input");if(firstInput){focusInput(firstInput);break}}if("function"===typeof timeoutCallback){setTimeout(timeoutCallback,0);break}}}while(element.previousSibling)}function focusInput(input){if(input.offsetParent!==null)input.focus();else triggerCustomEvent(document,"frmMaybeDelayFocus",{input})}function focusFieldFromErrorLink(event){const href=this.getAttribute("href");if(!href||!href.startsWith("#"))return; +const container=document.getElementById(href.substring(1));if(!container)return;event.preventDefault();container.scrollIntoView({behavior:"smooth",block:"center"});const input=getFocusableInputInField(container);if(input){focusInput(input);return}container.setAttribute("tabindex","-1");focusInput(container)}function getFocusableInputInField(container){const inputs=Array.from(container.querySelectorAll(FOCUSABLE_FIELD_SELECTOR)).filter(inputCanTakeFocus);if(!inputs.length)return null;const invalidInput= +inputs.find(input=>"true"===input.getAttribute("aria-invalid"));if(invalidInput)return invalidInput;return inputs.find(fieldInputIsEmpty)||inputs[0]}function fieldInputIsEmpty(input){if("BUTTON"===input.nodeName||["button","checkbox","file","radio","submit"].includes(input.type))return false;return""===String(input.value||"").trim()}function inputCanTakeFocus(input){if(input.disabled||"hidden"===input.type)return false;const rect=input.getBoundingClientRect();if(!rect.width&&!rect.height)return false; +return"hidden"!==getComputedStyle(input).visibility}function documentOn(event,selector,handler,options){if(options===undefined)options=false;document.addEventListener(event,function(e){let target;for(target=e.target;target&&target!=this;target=target.parentNode)if(target.matches&&target.matches(selector)){handler.call(target,e);break}},options)}function initFloatingLabels(){const selector=".frm-show-form .frm_inside_container input, .frm-show-form .frm_inside_container select, .frm-show-form .frm_inside_container textarea"; +const floatClass="frm_label_float_top";const checkFloatLabel=function(input){const container=input.closest(".frm_inside_container");if(!container)return;const shouldFloatTop=input.value||document.activeElement===input;container.classList.toggle(floatClass,shouldFloatTop);if("SELECT"===input.tagName){const firstOpt=input.querySelector("option:first-child");if(shouldFloatTop){if(firstOpt.hasAttribute("data-label")){firstOpt.textContent=firstOpt.getAttribute("data-label");firstOpt.removeAttribute("data-label")}}else if(firstOpt.textContent){firstOpt.setAttribute("data-label", +firstOpt.textContent);firstOpt.textContent=""}}};const checkDropdownLabel=function(){document.querySelectorAll(`.frm-show-form .frm_inside_container:not(.${floatClass}) select`).forEach(function(input){const firstOpt=input.querySelector("option:first-child");if(firstOpt.textContent){firstOpt.setAttribute("data-label",firstOpt.textContent);firstOpt.textContent=""}})};["focus","blur","change"].forEach(function(eventName){documentOn(eventName,selector,function(event){checkFloatLabel(event.target)},true)}); +const runOnLoad=function(firstLoad){if(firstLoad&&document.activeElement&&["INPUT","SELECT","TEXTAREA"].includes(document.activeElement.tagName))checkFloatLabel(document.activeElement);else if(firstLoad)document.querySelectorAll(".frm_inside_container").forEach(function(container){const input=container.querySelector("input, select, textarea");if(input&&""!==input.value)checkFloatLabel(input)});checkDropdownLabel();calcProductsTotal()};runOnLoad(true);jQuery(document).on("frmPageChanged",function(event){runOnLoad()}); document.addEventListener("frm_after_start_over",function(event){runOnLoad()})}function shouldUpdateValidityMessage(target){if("INPUT"!==target.nodeName)return false;if(!target.dataset.invmsg)return false;if("text"!==target.getAttribute("type"))return false;if(target.classList.contains("frm_verify"))return false;return true}function maybeClearCustomValidityMessage(event,field){let key;let isInvalid=false;if(!shouldUpdateValidityMessage(field))return;for(key in field.validity){if("customError"===key)continue; if("valid"!==key&&field.validity[key]===true){isInvalid=true;break}}if(!isInvalid)field.setCustomValidity("")}function maybeShowNewTabFallbackMessage(){if(!window.frmShowNewTabFallback)return;const messageEl=document.querySelector(`#frm_form_${frmShowNewTabFallback.formId}_container .frm_message`);if(!messageEl)return;messageEl.insertAdjacentHTML("beforeend",` ${frmShowNewTabFallback.message}`)}function setCustomValidityMessage(){const forms=document.getElementsByClassName("frm-show-form");const {length}= forms;for(let index=0;index Date: Wed, 30 Sep 2026 06:30:26 -0600 Subject: [PATCH 3/9] Address combo field review items in JS - Resolve the container for sub field errors from the input, so repeater sub fields keep their error on AJAX submit. - Use one key for the combined error element ID and its aria-describedby. - Validate required combo fields on the first blur, flagging only the changed sub field until an error exists. - With JS validation off, clear the combined error once it no longer applies. - Reuse removeFieldError and the sub input query/required errors already computed. Co-Authored-By: Claude Sonnet 5.5 --- js/formidable.js | 105 +++++++++++++++++++++++++++++++++---------- js/formidable.min.js | 46 ++++++++++--------- 2 files changed, 106 insertions(+), 45 deletions(-) diff --git a/js/formidable.js b/js/formidable.js index 38a0f42dec..410934d3d0 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -341,7 +341,7 @@ function frmFrontFormJS() { } const comboContainer = field.closest( '.frm_combo_inputs_container' ); - if ( comboContainer && comboFieldHasFieldError( comboContainer ) ) { + if ( comboContainer && ( isRequiredComboField( comboContainer ) || comboFieldHasFieldError( comboContainer ) ) ) { validateComboField( comboContainer, field, addErrors ); return; } @@ -369,6 +369,31 @@ function frmFrontFormJS() { } } + /** + * Gets the container to show the error for a key in. + * + * @since x.x + * + * @param {HTMLElement} form + * @param {string} key + * @return {HTMLElement|null} The container, or null if there is none. + */ + function getFieldContainerForErrorKey( form, key ) { + const container = form.querySelector( `#frm_field_${ key }_container` ); + if ( container ) { + return container; + } + + // A sub field container in a repeater does not have an ID that matches its key, so look it up from its input. + for ( const comboContainer of form.querySelectorAll( '.frm_combo_inputs_container' ) ) { + const input = getRequiredComboSubInputs( comboContainer ).find( subInput => key === getFieldId( subInput, true ) ); + if ( input ) { + return input.closest( '.frm_form_field' ); + } + } + return null; + } + /** * Gets the sub field inputs of a combo field, such as name or address, that must be filled in. * @@ -402,17 +427,17 @@ function frmFrontFormJS() { * @since x.x * * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. - * @param {Object} errors Errors keyed by field container key. Updated in place. + * @param {Object} errors Errors keyed by field container key, including the required errors of the sub fields. Updated in place. + * @param {HTMLElement[]} inputs Optional. The required sub inputs. Defaults to a query of the combo field. * @return {void} */ - function maybeCombineComboFieldErrors( comboContainer, errors ) { + function maybeCombineComboFieldErrors( comboContainer, errors, inputs = getRequiredComboSubInputs( comboContainer ) ) { const fieldContainer = comboContainer.closest( '.frm_form_field' ); - if ( ! fieldContainer || ! hasClass( fieldContainer, 'frm_required_field' ) ) { + if ( ! isRequiredComboField( comboContainer ) ) { return; } - const inputs = getRequiredComboSubInputs( comboContainer ); - const allEmpty = inputs.length && inputs.every( input => Object.keys( checkRequiredField( input, {} ) ).length ); + const allEmpty = inputs.length && inputs.every( input => getFieldId( input, true ) in errors ); if ( ! allEmpty ) { return; } @@ -427,7 +452,6 @@ function frmFrontFormJS() { message = errors[ key ]; } // An empty error still flags the sub field, without repeating the message under it. - // In a repeater the sub fields share the field key, so this is replaced below. errors[ key ] = ''; } ); @@ -450,16 +474,37 @@ function frmFrontFormJS() { return ''; } + return wrapErrorHtml( message, subInput, getComboFieldErrorKey( subInput ) ); + } + + /** + * Gets the key used in the error element ID for the whole combo field. + * + * @since x.x + * + * @param {HTMLElement} subInput A sub field input of the combo field. + * @return {string} The field HTML ID without its 'field_' prefix. + */ + function getComboFieldErrorKey( subInput ) { const subFieldContainer = subInput.closest( '[data-sub-field-name]' ); const subFieldName = subFieldContainer ? subFieldContainer.getAttribute( 'data-sub-field-name' ) : ''; - // The sub input ID is the field HTML ID plus '_{sub field name}'. The error for the whole - // field is keyed by the field HTML ID without its 'field_' prefix. + // The sub input ID is the field HTML ID plus '_{sub field name}'. let errorKey = subInput.id.replace( /^field_/, '' ); if ( subFieldName && errorKey.endsWith( `_${ subFieldName }` ) ) { errorKey = errorKey.slice( 0, -( subFieldName.length + 1 ) ); } + return errorKey; + } - return wrapErrorHtml( message, subInput, errorKey ); + /** + * @since x.x + * + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @return {boolean} True if the combo field is required. + */ + function isRequiredComboField( comboContainer ) { + const fieldContainer = comboContainer.closest( '.frm_form_field' ); + return null !== fieldContainer && hasClass( fieldContainer, 'frm_required_field' ); } /** @@ -497,13 +542,15 @@ function frmFrontFormJS() { */ function validateComboField( comboContainer, field, addErrors ) { const fieldContainer = comboContainer.closest( '.frm_form_field' ); + const hadError = comboFieldHasFieldError( comboContainer ); + const inputs = getRequiredComboSubInputs( comboContainer ); let errors = {}; - if ( hasClass( fieldContainer, 'frm_required_field' ) ) { - getRequiredComboSubInputs( comboContainer ).forEach( input => { + if ( isRequiredComboField( comboContainer ) ) { + inputs.forEach( input => { errors = checkRequiredField( input, errors ); } ); - maybeCombineComboFieldErrors( comboContainer, errors ); + maybeCombineComboFieldErrors( comboContainer, errors, inputs ); } if ( ! ( getFieldId( field, true ) in errors ) ) { @@ -519,6 +566,16 @@ function frmFrontFormJS() { if ( subFieldContainer !== fieldContainer && ! ( fieldKey in errors ) && ! ( getFieldId( field, true ) in errors ) ) { removeFieldError( subFieldContainer ); } + + if ( ! ( fieldKey in errors ) && hasClass( fieldContainer, 'frm_blank_field' ) ) { + // The error for the whole field no longer applies. Keep the flags on the sub fields that still fail. + const failing = inputs.filter( input => getFieldId( input, true ) in errors ); + removeComboFieldErrors( comboContainer ); + failing.forEach( input => { + input.closest( '.frm_form_field' ).classList.add( 'frm_blank_field' ); + input.setAttribute( 'aria-invalid', 'true' ); + } ); + } return; } @@ -532,13 +589,18 @@ function frmFrontFormJS() { // lookup - the id lookup can miss inside a repeating section, where a sub field // container's id does not necessarily include the repeating row (see getFieldId()). const subFieldContainers = {}; - getRequiredComboSubInputs( comboContainer ).forEach( input => { + inputs.forEach( input => { subFieldContainers[ getFieldId( input, true ) ] = input.closest( '.frm_form_field' ); } ); + // The first time a combo field is validated, only flag the sub field that changed, not the + // ones the person has not reached yet. + const changedKey = getFieldId( field, true ); + const keys = Object.keys( errors ).filter( key => hadError || key === changedKey || key === fieldKey ); + const form = fieldContainer.closest( 'form' ); - Object.keys( errors ).forEach( key => { - const container = key === fieldKey ? fieldContainer : ( subFieldContainers[ key ] || form?.querySelector( `#frm_field_${ key }_container` ) ); + keys.forEach( key => { + const container = key === fieldKey ? fieldContainer : ( subFieldContainers[ key ] || ( form && getFieldContainerForErrorKey( form, key ) ) ); if ( container ) { addFieldError( container, key, errors ); } @@ -557,11 +619,7 @@ function frmFrontFormJS() { const fieldContainer = comboContainer.closest( '.frm_form_field' ); comboContainer.querySelectorAll( '.frm_form_field' ).forEach( removeFieldError ); - fieldContainer.classList.remove( 'frm_blank_field', 'has-error' ); - fieldContainer.querySelectorAll( ':scope > .frm_error' ).forEach( errorMessage => { - removeElementFromInputDescribedBy( errorMessage ); - errorMessage.remove(); - } ); + removeFieldError( fieldContainer ); comboContainer.querySelectorAll( '[aria-invalid="true"]' ).forEach( input => input.setAttribute( 'aria-invalid', 'false' ) ); } @@ -1441,7 +1499,8 @@ function frmFrontFormJS() { container.classList.add( 'frm_blank_field' ); const allInputs = container.querySelectorAll( 'input, select, textarea' ); - const id = getErrorElementId( key, allInputs[ 0 ] ); + const isComboField = key === getFieldContainerErrorKey( container ) && null !== container.querySelector( '.frm_combo_inputs_container' ); + const id = isComboField ? `frm_error_field_${ getComboFieldErrorKey( allInputs[ 0 ] ) }` : getErrorElementId( key, allInputs[ 0 ] ); // An error for a whole combo field, such as an address, does not apply to its optional sub fields. const inputs = Array.from( allInputs ).filter( input => ! hasClass( input, 'frm_optional' ) || input.closest( '.frm_form_field' ) === container @@ -2845,7 +2904,7 @@ function frmFrontFormJS() { removeAllErrors(); for ( key in jsErrors ) { - const fieldCont = form ? form.querySelector( `#frm_field_${ key }_container` ) : null; + const fieldCont = form ? getFieldContainerForErrorKey( form, key ) : null; if ( fieldCont ) { addFieldError( fieldCont, key, jsErrors ); diff --git a/js/formidable.min.js b/js/formidable.min.js index a3fccd0eb5..8ec138d759 100644 --- a/js/formidable.min.js +++ b/js/formidable.min.js @@ -6,15 +6,17 @@ link.style.pointerEvents="none")}function enableSaveDraft($form){const form=$for null;if(!isVisible)return;requiredField.querySelectorAll("input, select, textarea").forEach(requiredInput=>{if(hasClass(requiredInput,"frm_optional")||hasClass(requiredInput,"ed_button"))return;errors=checkRequiredField(requiredInput,errors)})});vanillaJsObject?.querySelectorAll(".frm_required_field .frm_combo_inputs_container").forEach(comboContainer=>maybeCombineComboFieldErrors(comboContainer,errors));vanillaJsObject?.querySelectorAll("input,select,textarea").forEach(field=>{if(""===field.value){if("number"=== field.type)checkValidity(field,errors);const isConfirmationField=field.name&&0===field.name.indexOf("item_meta[conf_");if(!isConfirmationField)return}validateFieldValue(field,errors,true);checkValidity(field,errors)});if(!hasInvisibleRecaptcha(object))errors=validateRecaptcha(object,errors);return errors}function checkValidity(field,errors){if("object"!==typeof field.validity||false!==field.validity.valid)return;const fieldID=getFieldId(field,true);if(errors[fieldID]===undefined)errors[fieldID]=getFieldValidationMessage(field, "data-invmsg");if("function"===typeof field.reportValidity)field.reportValidity()}function hasClass(element,targetClass){return element.classList&&element.classList.contains(targetClass)}function maybeValidateChange(field){if(field.type==="url")maybeAddHttpsToUrl(field);const form=field.closest("form");if(!form)return;validateField(field,hasClass(form,"frm_js_validate"))}function maybeAddHttpsToUrl(field){const url=field.value;const matches=url.match(/^(https?|ftps?|mailto|news|feed|telnet):/);if(field.value!== -""&&matches===null)field.value=`https://${url}`}function validateField(field,addErrors=true){let errors;let key;errors=[];const fieldContainer=field.closest(".frm_form_field");if(!fieldContainer)return;const comboContainer=field.closest(".frm_combo_inputs_container");if(comboContainer&&comboFieldHasFieldError(comboContainer)){validateComboField(comboContainer,field,addErrors);return}if(hasClass(fieldContainer,"frm_required_field")&&!hasClass(field,"frm_optional"))errors=checkRequiredField(field,errors); -if(errors.length<1)validateFieldValue(field,errors,false);const hasErrors=Object.keys(errors).length>0;if(addErrors){removeFieldError(fieldContainer);if(hasErrors)for(key in errors)addFieldError(fieldContainer,key,errors)}else if(!hasErrors)removeFieldError(fieldContainer)}function getRequiredComboSubInputs(comboContainer){return Array.from(comboContainer.querySelectorAll("input, select, textarea")).filter(input=>"hidden"!==input.type&&!hasClass(input,"frm_optional")&&null!==input.offsetParent)}function getFieldContainerErrorKey(fieldContainer){return fieldContainer.id.replace(/^frm_field_/, -"").replace(/_container$/,"")}function maybeCombineComboFieldErrors(comboContainer,errors){const fieldContainer=comboContainer.closest(".frm_form_field");if(!fieldContainer||!hasClass(fieldContainer,"frm_required_field"))return;const inputs=getRequiredComboSubInputs(comboContainer);const allEmpty=inputs.length&&inputs.every(input=>Object.keys(checkRequiredField(input,{})).length);if(!allEmpty)return;const fieldKey=getFieldContainerErrorKey(fieldContainer);let message=getComboFieldRequiredMessage(comboContainer, -inputs[0]);inputs.forEach(input=>{const key=getFieldId(input,true);if(""===message&&errors[key])message=errors[key];errors[key]=""});errors[fieldKey]=message}function getComboFieldRequiredMessage(comboContainer,subInput){const message=comboContainer.getAttribute("data-reqmsg");if(!message)return"";const subFieldContainer=subInput.closest("[data-sub-field-name]");const subFieldName=subFieldContainer?subFieldContainer.getAttribute("data-sub-field-name"):"";let errorKey=subInput.id.replace(/^field_/, -"");if(subFieldName&&errorKey.endsWith(`_${subFieldName}`))errorKey=errorKey.slice(0,-(subFieldName.length+1));return wrapErrorHtml(message,subInput,errorKey)}function comboFieldHasFieldError(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");if(fieldContainer&&hasClass(fieldContainer,"frm_blank_field")&&null!==fieldContainer.querySelector(":scope > .frm_error"))return true;return null!==comboContainer.querySelector(".frm_form_field.frm_blank_field")}function validateComboField(comboContainer, -field,addErrors){const fieldContainer=comboContainer.closest(".frm_form_field");let errors={};if(hasClass(fieldContainer,"frm_required_field")){getRequiredComboSubInputs(comboContainer).forEach(input=>{errors=checkRequiredField(input,errors)});maybeCombineComboFieldErrors(comboContainer,errors)}if(!(getFieldId(field,true)in errors))validateFieldValue(field,errors,false);const fieldKey=getFieldContainerErrorKey(fieldContainer);const hasErrors=Object.keys(errors).length>0;if(!addErrors&&hasErrors){const subFieldContainer= -field.closest(".frm_form_field");if(subFieldContainer!==fieldContainer&&!(fieldKey in errors)&&!(getFieldId(field,true)in errors))removeFieldError(subFieldContainer);return}removeComboFieldErrors(comboContainer);if(!addErrors)return;const subFieldContainers={};getRequiredComboSubInputs(comboContainer).forEach(input=>{subFieldContainers[getFieldId(input,true)]=input.closest(".frm_form_field")});const form=fieldContainer.closest("form");Object.keys(errors).forEach(key=>{const container=key===fieldKey? -fieldContainer:subFieldContainers[key]||form?.querySelector(`#frm_field_${key}_container`);if(container)addFieldError(container,key,errors)})}function removeComboFieldErrors(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");comboContainer.querySelectorAll(".frm_form_field").forEach(removeFieldError);fieldContainer.classList.remove("frm_blank_field","has-error");fieldContainer.querySelectorAll(":scope > .frm_error").forEach(errorMessage=>{removeElementFromInputDescribedBy(errorMessage); -errorMessage.remove()});comboContainer.querySelectorAll('[aria-invalid="true"]').forEach(input=>input.setAttribute("aria-invalid","false"))}function validateFieldValue(field,errors,onSubmit){if(field.type==="hidden");else if(field.type==="number")checkNumberField(field,errors);else if(field.type==="email")checkEmailField(field,errors,onSubmit);else if(field.type==="password")checkPasswordField(field,errors,onSubmit);else if(field.type==="url")checkUrlField(field,errors);else if(field.pattern!==null)checkPatternField(field, +""&&matches===null)field.value=`https://${url}`}function validateField(field,addErrors=true){let errors;let key;errors=[];const fieldContainer=field.closest(".frm_form_field");if(!fieldContainer)return;const comboContainer=field.closest(".frm_combo_inputs_container");if(comboContainer&&(isRequiredComboField(comboContainer)||comboFieldHasFieldError(comboContainer))){validateComboField(comboContainer,field,addErrors);return}if(hasClass(fieldContainer,"frm_required_field")&&!hasClass(field,"frm_optional"))errors= +checkRequiredField(field,errors);if(errors.length<1)validateFieldValue(field,errors,false);const hasErrors=Object.keys(errors).length>0;if(addErrors){removeFieldError(fieldContainer);if(hasErrors)for(key in errors)addFieldError(fieldContainer,key,errors)}else if(!hasErrors)removeFieldError(fieldContainer)}function getFieldContainerForErrorKey(form,key){const container=form.querySelector(`#frm_field_${key}_container`);if(container)return container;for(const comboContainer of form.querySelectorAll(".frm_combo_inputs_container")){const input= +getRequiredComboSubInputs(comboContainer).find(subInput=>key===getFieldId(subInput,true));if(input)return input.closest(".frm_form_field")}return null}function getRequiredComboSubInputs(comboContainer){return Array.from(comboContainer.querySelectorAll("input, select, textarea")).filter(input=>"hidden"!==input.type&&!hasClass(input,"frm_optional")&&null!==input.offsetParent)}function getFieldContainerErrorKey(fieldContainer){return fieldContainer.id.replace(/^frm_field_/,"").replace(/_container$/, +"")}function maybeCombineComboFieldErrors(comboContainer,errors,inputs=getRequiredComboSubInputs(comboContainer)){const fieldContainer=comboContainer.closest(".frm_form_field");if(!isRequiredComboField(comboContainer))return;const allEmpty=inputs.length&&inputs.every(input=>getFieldId(input,true)in errors);if(!allEmpty)return;const fieldKey=getFieldContainerErrorKey(fieldContainer);let message=getComboFieldRequiredMessage(comboContainer,inputs[0]);inputs.forEach(input=>{const key=getFieldId(input, +true);if(""===message&&errors[key])message=errors[key];errors[key]=""});errors[fieldKey]=message}function getComboFieldRequiredMessage(comboContainer,subInput){const message=comboContainer.getAttribute("data-reqmsg");if(!message)return"";return wrapErrorHtml(message,subInput,getComboFieldErrorKey(subInput))}function getComboFieldErrorKey(subInput){const subFieldContainer=subInput.closest("[data-sub-field-name]");const subFieldName=subFieldContainer?subFieldContainer.getAttribute("data-sub-field-name"): +"";let errorKey=subInput.id.replace(/^field_/,"");if(subFieldName&&errorKey.endsWith(`_${subFieldName}`))errorKey=errorKey.slice(0,-(subFieldName.length+1));return errorKey}function isRequiredComboField(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");return null!==fieldContainer&&hasClass(fieldContainer,"frm_required_field")}function comboFieldHasFieldError(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");if(fieldContainer&&hasClass(fieldContainer, +"frm_blank_field")&&null!==fieldContainer.querySelector(":scope > .frm_error"))return true;return null!==comboContainer.querySelector(".frm_form_field.frm_blank_field")}function validateComboField(comboContainer,field,addErrors){const fieldContainer=comboContainer.closest(".frm_form_field");const hadError=comboFieldHasFieldError(comboContainer);const inputs=getRequiredComboSubInputs(comboContainer);let errors={};if(isRequiredComboField(comboContainer)){inputs.forEach(input=>{errors=checkRequiredField(input, +errors)});maybeCombineComboFieldErrors(comboContainer,errors,inputs)}if(!(getFieldId(field,true)in errors))validateFieldValue(field,errors,false);const fieldKey=getFieldContainerErrorKey(fieldContainer);const hasErrors=Object.keys(errors).length>0;if(!addErrors&&hasErrors){const subFieldContainer=field.closest(".frm_form_field");if(subFieldContainer!==fieldContainer&&!(fieldKey in errors)&&!(getFieldId(field,true)in errors))removeFieldError(subFieldContainer);if(!(fieldKey in errors)&&hasClass(fieldContainer, +"frm_blank_field")){const failing=inputs.filter(input=>getFieldId(input,true)in errors);removeComboFieldErrors(comboContainer);failing.forEach(input=>{input.closest(".frm_form_field").classList.add("frm_blank_field");input.setAttribute("aria-invalid","true")})}return}removeComboFieldErrors(comboContainer);if(!addErrors)return;const subFieldContainers={};inputs.forEach(input=>{subFieldContainers[getFieldId(input,true)]=input.closest(".frm_form_field")});const changedKey=getFieldId(field,true);const keys= +Object.keys(errors).filter(key=>hadError||key===changedKey||key===fieldKey);const form=fieldContainer.closest("form");keys.forEach(key=>{const container=key===fieldKey?fieldContainer:subFieldContainers[key]||form&&getFieldContainerForErrorKey(form,key);if(container)addFieldError(container,key,errors)})}function removeComboFieldErrors(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");comboContainer.querySelectorAll(".frm_form_field").forEach(removeFieldError);removeFieldError(fieldContainer); +comboContainer.querySelectorAll('[aria-invalid="true"]').forEach(input=>input.setAttribute("aria-invalid","false"))}function validateFieldValue(field,errors,onSubmit){if(field.type==="hidden");else if(field.type==="number")checkNumberField(field,errors);else if(field.type==="email")checkEmailField(field,errors,onSubmit);else if(field.type==="password")checkPasswordField(field,errors,onSubmit);else if(field.type==="url")checkUrlField(field,errors);else if(field.pattern!==null)checkPatternField(field, errors);if("tel"===field.type&&shouldCheckConfirmField(field,onSubmit))confirmField(field,errors);triggerCustomEvent(document,"frm_validate_field_value",{field,errors,onSubmit})}function checkRequiredField(field,errors){let tempVal;let i;let placeholder;let val="";let fieldID="";let fileID=field.getAttribute("data-frmfile");if(field.type==="hidden"&&fileID===null&&!isAppointmentField(field)&&!isInlineDatepickerField(field))return errors;if(field.type==="checkbox"||field.type==="radio")document.querySelectorAll(`input[name="${field.name}"]`).forEach(function(input){const requiredField= input.closest(".frm_required_field");if(!requiredField)return;const checkedInputs=requiredField.querySelectorAll("input:checked");checkedInputs.forEach(function(checkedInput){val=checkedInput.value})});else if(field.type==="file"||fileID){if(fileID===undefined){fileID=getFieldId(field,true);fileID=fileID.replace("file","")}if(errors[fileID]===undefined)val=getFileVals(fileID);fieldID=fileID}else{if(hasClass(field,"frm_pos_none"))return errors;val=jQuery(field).val();if(val===null)val="";else if(typeof val!== "string"){tempVal=val;val="";for(i=0;iel.remove())}}function maybeSlideOut(oldContent,newContent){let c;let newClass="frm_slideout";if(newContent.includes(" frm_slide")){c=oldContent.children();if(newContent.includes(" frm_going_back"))newClass+=" frm_going_back";c.removeClass("frm_going_back");c.addClass(newClass);return 300}return 0}function addUrlParam(response){let url;if(history.pushState&&response.page!==undefined){url=addQueryVar("frm_page",response.page);window.history.pushState({html:response.html},"", `?${url}`)}}function addQueryVar(key,value){key=encodeURI(key);value=encodeURI(value);const kvp=document.location.search.substr(1).split("&");let i=kvp.length;while(i--){const x=kvp[i].split("=");if(x[0]==key){x[1]=value;kvp[i]=x.join("=");break}}if(i<0)kvp[kvp.length]=[key,value].join("=");return kvp.join("&")}function getErrorConfigForForm(formEl){const fallback={includeAlertRole:!!frm_js.include_alert_role,focusFirstError:!!frm_js.focus_first_error,focusErrorSummary:false};if(!formEl||!formEl.dataset.frmErrorConfig)return fallback; if(!formEl.frmErrorConfigCache)try{formEl.frmErrorConfigCache=JSON.parse(formEl.dataset.frmErrorConfig)}catch(e){formEl.frmErrorConfigCache=fallback}return formEl.frmErrorConfigCache}function insertErrorHtml(container,errorHtml){const template=document.createElement("template");template.innerHTML=errorHtml;Array.from(template.content.children).forEach(el=>el.setAttribute("data-frm-error",""));container.append(template.content)}function addFieldError($fieldCont,key,jsErrors){const container=$fieldCont instanceof -jQuery?$fieldCont.get(0):$fieldCont;if(!container||container.offsetParent===null)return;container.classList.add("frm_blank_field");const allInputs=container.querySelectorAll("input, select, textarea");const id=getErrorElementId(key,allInputs[0]);const inputs=Array.from(allInputs).filter(input=>!hasClass(input,"frm_optional")||input.closest(".frm_form_field")===container);let describedBy;if(typeof frmThemeOverride_frmPlaceError==="function")frmThemeOverride_frmPlaceError(key,jsErrors);else if(""=== -jsErrors[key]);else{let errorHtml;if(jsErrors[key].includes("${jsErrors[key]}`}insertErrorHtml(container,errorHtml);inputs.forEach(input=>{describedBy=input.getAttribute("aria-describedby");if(!describedBy)describedBy=id;else if(!describedBy.includes(id)&&!describedBy.includes("frm_error_field_")){const {errorFirst}= -input.dataset;if(errorFirst==="0")describedBy=`${describedBy} ${id}`;else describedBy=`${id} ${describedBy}`}input.setAttribute("aria-describedby",describedBy)})}inputs.forEach(input=>{if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","true")}else input.setAttribute("aria-invalid","true")});jQuery(document).trigger("frmAddFieldError",[jQuery(container),key,jsErrors])}function getErrorElementId(key, -input){if(isNaN(key)||!input||!input.id)return`frm_error_field_${key}`;return`frm_error_${input.id}`}function removeFieldError(fieldCont){const container=fieldCont instanceof jQuery?fieldCont.get(0):fieldCont;if(!container)return;const errorMessages=container.querySelectorAll(".frm_error, [data-frm-error]");const input=container.querySelector("input, select, textarea");container.classList.remove("frm_blank_field","has-error");if(input)if("true"===input.getAttribute("aria-invalid"))input.setAttribute("aria-invalid", -"false");else if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","false")}errorMessages.forEach(errorMessage=>{removeElementFromInputDescribedBy(errorMessage);errorMessage.remove()})}function removeElementFromInputDescribedBy(el){document.querySelectorAll(`[aria-describedby*="${el.id}"]`).forEach(input=>{let ariaDescribedBy=input.getAttribute("aria-describedby").split(" ");ariaDescribedBy=ariaDescribedBy.filter(value=> -{const trimmedValue=value.trim();return trimmedValue&&trimmedValue!==el.id});if(ariaDescribedBy.length){input.setAttribute("aria-describedby",ariaDescribedBy.join(" "));return}input.removeAttribute("aria-describedby")})}function removeAllErrors(){document.querySelectorAll(".form-field").forEach(field=>{field.classList.remove("frm_blank_field","has-error")});document.querySelectorAll(".form-field .frm_error, .form-field [data-frm-error]").forEach(el=>{removeElementFromInputDescribedBy(el);el.remove()}); -document.querySelectorAll(".frm_error_style").forEach(error=>error.remove())}function scrollToFirstField(object){if("function"===typeof object.get)object=object.get(0);const field=object.querySelector(".frm_blank_field");if(field)frmFrontForm.scrollMsg(jQuery(field),object,true)}function showSubmitLoading($object){showLoadingIndicator($object);disableSubmitButton($object);disableSaveDraft($object)}function showLoadingIndicator($object){if(!$object.hasClass("frm_loading_form")&&!$object.hasClass("frm_loading_prev")){addLoadingClass($object); -$object.trigger("frmStartFormLoading")}}function addLoadingClass($object){const loadingClass=isGoingToPrevPage($object)?"frm_loading_prev":"frm_loading_form";$object.addClass(loadingClass)}function isGoingToPrevPage($object){return typeof frmProForm!=="undefined"&&frmProForm.goingToPreviousPage($object)}function removeSubmitLoading(_,enable,processesRunning){if(processesRunning>0)return;document.querySelectorAll(".frm_loading_form").forEach(function(form){form.classList.remove("frm_loading_form", +jQuery?$fieldCont.get(0):$fieldCont;if(!container||container.offsetParent===null)return;container.classList.add("frm_blank_field");const allInputs=container.querySelectorAll("input, select, textarea");const isComboField=key===getFieldContainerErrorKey(container)&&null!==container.querySelector(".frm_combo_inputs_container");const id=isComboField?`frm_error_field_${getComboFieldErrorKey(allInputs[0])}`:getErrorElementId(key,allInputs[0]);const inputs=Array.from(allInputs).filter(input=>!hasClass(input, +"frm_optional")||input.closest(".frm_form_field")===container);let describedBy;if(typeof frmThemeOverride_frmPlaceError==="function")frmThemeOverride_frmPlaceError(key,jsErrors);else if(""===jsErrors[key]);else{let errorHtml;if(jsErrors[key].includes("${jsErrors[key]}`}insertErrorHtml(container, +errorHtml);inputs.forEach(input=>{describedBy=input.getAttribute("aria-describedby");if(!describedBy)describedBy=id;else if(!describedBy.includes(id)&&!describedBy.includes("frm_error_field_")){const {errorFirst}=input.dataset;if(errorFirst==="0")describedBy=`${describedBy} ${id}`;else describedBy=`${id} ${describedBy}`}input.setAttribute("aria-describedby",describedBy)})}inputs.forEach(input=>{if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]'); +if(group)group.setAttribute("aria-invalid","true")}else input.setAttribute("aria-invalid","true")});jQuery(document).trigger("frmAddFieldError",[jQuery(container),key,jsErrors])}function getErrorElementId(key,input){if(isNaN(key)||!input||!input.id)return`frm_error_field_${key}`;return`frm_error_${input.id}`}function removeFieldError(fieldCont){const container=fieldCont instanceof jQuery?fieldCont.get(0):fieldCont;if(!container)return;const errorMessages=container.querySelectorAll(".frm_error, [data-frm-error]"); +const input=container.querySelector("input, select, textarea");container.classList.remove("frm_blank_field","has-error");if(input)if("true"===input.getAttribute("aria-invalid"))input.setAttribute("aria-invalid","false");else if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","false")}errorMessages.forEach(errorMessage=>{removeElementFromInputDescribedBy(errorMessage);errorMessage.remove()})}function removeElementFromInputDescribedBy(el){document.querySelectorAll(`[aria-describedby*="${el.id}"]`).forEach(input=> +{let ariaDescribedBy=input.getAttribute("aria-describedby").split(" ");ariaDescribedBy=ariaDescribedBy.filter(value=>{const trimmedValue=value.trim();return trimmedValue&&trimmedValue!==el.id});if(ariaDescribedBy.length){input.setAttribute("aria-describedby",ariaDescribedBy.join(" "));return}input.removeAttribute("aria-describedby")})}function removeAllErrors(){document.querySelectorAll(".form-field").forEach(field=>{field.classList.remove("frm_blank_field","has-error")});document.querySelectorAll(".form-field .frm_error, .form-field [data-frm-error]").forEach(el=> +{removeElementFromInputDescribedBy(el);el.remove()});document.querySelectorAll(".frm_error_style").forEach(error=>error.remove())}function scrollToFirstField(object){if("function"===typeof object.get)object=object.get(0);const field=object.querySelector(".frm_blank_field");if(field)frmFrontForm.scrollMsg(jQuery(field),object,true)}function showSubmitLoading($object){showLoadingIndicator($object);disableSubmitButton($object);disableSaveDraft($object)}function showLoadingIndicator($object){if(!$object.hasClass("frm_loading_form")&& +!$object.hasClass("frm_loading_prev")){addLoadingClass($object);$object.trigger("frmStartFormLoading")}}function addLoadingClass($object){const loadingClass=isGoingToPrevPage($object)?"frm_loading_prev":"frm_loading_form";$object.addClass(loadingClass)}function isGoingToPrevPage($object){return typeof frmProForm!=="undefined"&&frmProForm.goingToPreviousPage($object)}function removeSubmitLoading(_,enable,processesRunning){if(processesRunning>0)return;document.querySelectorAll(".frm_loading_form").forEach(function(form){form.classList.remove("frm_loading_form", "frm_loading_prev");jQuery(form).trigger("frmEndFormLoading");if(enable==="enable"){enableSubmitButton(form);enableSaveDraft(form)}})}function showFileLoading(object){const loading=document.getElementById("frm_loading");if(!loading)return;const fileInput=object.querySelector("input[type=file]");const fileval=fileInput?fileInput.value:"";if(fileval!=="")setTimeout(function(){jQuery(loading).fadeIn("slow")},2E3)}function confirmClick(){const message=this.dataset.frmconfirm;return confirm(message)}function onHoneypotFieldChange(){const css= window.getComputedStyle(this).boxShadow;if(css?.match(/inset/))this.remove()}function changeFocusWhenClickComboFieldLabel(){let label;const comboInputsContainer=document.querySelectorAll(".frm_combo_inputs_container");comboInputsContainer.forEach(function(inputsContainer){if(!inputsContainer.closest(".frm_form_field"))return;label=inputsContainer.closest(".frm_form_field").querySelector(".frm_primary_label");if(!label)return;label.addEventListener("click",function(){inputsContainer.querySelector(".frm_form_field:first-child input, .frm_form_field:first-child select, .frm_form_field:first-child textarea").focus()})})} function maybeFocusOnComboSubField(element){if("FIELDSET"!==element.nodeName)return false;if(!element.querySelector(".frm_combo_inputs_container"))return false;const comboSubfield=element.querySelector('[aria-invalid="true"]');if(comboSubfield){focusInput(comboSubfield);return true}return false}function checkForErrorsAndMaybeSetFocus(){const errors=document.querySelectorAll(".frm_form_field .frm_error, .frm_form_field [data-frm-error]");if(!errors.length)return;const formContainer=errors[0].closest(".frm-show-form"); @@ -90,11 +92,11 @@ const errors=frmFrontForm.validateFormSubmit(object);if(Object.keys(errors).leng document.createElement("input");antispamInput.type="hidden";antispamInput.name="antispam_token";antispamInput.value=object.getAttribute("data-token");object.append(antispamInput)}const uniqueIDInput=document.createElement("input");uniqueIDInput.type="hidden";uniqueIDInput.name="unique_id";uniqueIDInput.value=getUniqueKey();object.append(uniqueIDInput);if(classList.includes("frm_ajax_submit")){const fileInputs=object.querySelectorAll('input[type="file"]');hasFileFields=Array.from(fileInputs).filter(input=> !!input.value).length;if(hasFileFields<1){const actionInput=object.querySelector('input[name="frm_action"]');const action=actionInput?actionInput.value:"";frmFrontForm.checkFormErrors(object,action)}else object.submit()}else object.submit()},validateFormSubmit(object){const form=object instanceof jQuery?object.get(0):object;if(typeof tinyMCE!=="undefined"&&form?.querySelector(".wp-editor-wrap"))tinyMCE.triggerSave();jsErrors=[];if(shouldJSValidate(object)){frmFrontForm.getAjaxFormErrors(object);if(Object.keys(jsErrors).length)frmFrontForm.addAjaxFormErrors(object)}return jsErrors}, getAjaxFormErrors(object){let customErrors;let key;const form=object instanceof jQuery?object.get(0):object;jsErrors=validateForm(object);if(typeof frmThemeOverride_jsErrors==="function"){const actionInput=form?form.querySelector('input[name="frm_action"]'):null;const action=actionInput?actionInput.value:"";customErrors=frmThemeOverride_jsErrors(action,object);if(Object.keys(customErrors).length)for(key in customErrors)jsErrors[key]=customErrors[key]}triggerCustomEvent(document,"frm_get_ajax_form_errors", -{formEl:object,errors:jsErrors});return jsErrors},addAjaxFormErrors(object){let key;const form=object instanceof jQuery?object.get(0):object;removeAllErrors();for(key in jsErrors){const fieldCont=form?form.querySelector(`#frm_field_${key}_container`):null;if(fieldCont)addFieldError(fieldCont,key,jsErrors);else delete jsErrors[key]}scrollToFirstField(object);checkForErrorsAndMaybeSetFocus()},checkFormErrors:getFormErrors,checkRequiredField,showSubmitLoading,removeSubmitLoading,scrollToID(id){const object= -jQuery(document.getElementById(id));frmFrontForm.scrollMsg(object,false)},scrollMsg(id,object,animate){let newPos;let screenTop;let screenBottom;let scrollObj="";if(object===undefined){scrollObj=jQuery(document.getElementById(`frm_form_${id}_container`));if(scrollObj.length<1)return}else if(typeof id==="string"){const formEl=object instanceof jQuery?object.get(0):object;const fieldEl=formEl?formEl.querySelector(`#frm_field_${id}_container`):null;scrollObj=fieldEl?jQuery(fieldEl):jQuery()}else scrollObj= -id;jQuery(scrollObj).trigger("focus");newPos=scrollObj.offset().top;if(!newPos||frm_js.offset==="-1")return;newPos=newPos-frm_js.offset;const docMarginTop=getComputedStyle(document.documentElement).marginTop;const bodyMarginTop=getComputedStyle(document.body).marginTop;if(docMarginTop||bodyMarginTop)newPos=newPos-parseInt(docMarginTop)-parseInt(bodyMarginTop);if(newPos&&window.innerHeight){screenTop=document.documentElement.scrollTop||document.body.scrollTop;screenBottom=screenTop+window.innerHeight; -if(newPos>screenBottom||newPos/g,">").replace(/"/g,""").replace(/'/g,"'")},triggerCustomEvent,documentOn}}window.frmFrontForm=frmFrontFormJS();jQuery(document).ready(function(){frmFrontForm.init()});function frmRecaptcha(){frmCaptcha(".frm-g-recaptcha")}function frmHcaptcha(){frmCaptcha(".h-captcha")}function frmTurnstile(){frmCaptcha(".frm-cf-turnstile")} +{formEl:object,errors:jsErrors});return jsErrors},addAjaxFormErrors(object){let key;const form=object instanceof jQuery?object.get(0):object;removeAllErrors();for(key in jsErrors){const fieldCont=form?getFieldContainerForErrorKey(form,key):null;if(fieldCont)addFieldError(fieldCont,key,jsErrors);else delete jsErrors[key]}scrollToFirstField(object);checkForErrorsAndMaybeSetFocus()},checkFormErrors:getFormErrors,checkRequiredField,showSubmitLoading,removeSubmitLoading,scrollToID(id){const object=jQuery(document.getElementById(id)); +frmFrontForm.scrollMsg(object,false)},scrollMsg(id,object,animate){let newPos;let screenTop;let screenBottom;let scrollObj="";if(object===undefined){scrollObj=jQuery(document.getElementById(`frm_form_${id}_container`));if(scrollObj.length<1)return}else if(typeof id==="string"){const formEl=object instanceof jQuery?object.get(0):object;const fieldEl=formEl?formEl.querySelector(`#frm_field_${id}_container`):null;scrollObj=fieldEl?jQuery(fieldEl):jQuery()}else scrollObj=id;jQuery(scrollObj).trigger("focus"); +newPos=scrollObj.offset().top;if(!newPos||frm_js.offset==="-1")return;newPos=newPos-frm_js.offset;const docMarginTop=getComputedStyle(document.documentElement).marginTop;const bodyMarginTop=getComputedStyle(document.body).marginTop;if(docMarginTop||bodyMarginTop)newPos=newPos-parseInt(docMarginTop)-parseInt(bodyMarginTop);if(newPos&&window.innerHeight){screenTop=document.documentElement.scrollTop||document.body.scrollTop;screenBottom=screenTop+window.innerHeight;if(newPos>screenBottom||newPos/g, +">").replace(/"/g,""").replace(/'/g,"'")},triggerCustomEvent,documentOn}}window.frmFrontForm=frmFrontFormJS();jQuery(document).ready(function(){frmFrontForm.init()});function frmRecaptcha(){frmCaptcha(".frm-g-recaptcha")}function frmHcaptcha(){frmCaptcha(".h-captcha")}function frmTurnstile(){frmCaptcha(".frm-cf-turnstile")} function frmCaptcha(captchaSelector){if(".h-captcha"===captchaSelector){const captchaLabels=document.querySelectorAll('label[for="h-captcha-response"]');if(captchaLabels.length)captchaLabels.forEach(label=>{const captchaResponse=label.closest("form")?.querySelector('[name="h-captcha-response"]');if(captchaResponse)label.htmlFor=captchaResponse.id});return}let c;const captchas=document.querySelectorAll(captchaSelector);const cl=captchas.length;for(c=0;c Date: Wed, 30 Sep 2026 06:31:27 -0600 Subject: [PATCH 4/9] Address combo field review items in PHP and CSS - Apply the custom [if error] wrapper to combo sub field errors on AJAX submit. - Cover textarea, rich text, recaptcha, Stripe and chosen.js in the sub field override. - Fall back to the plain sub field label when the description is not a short phrase. - Share the name-override error message helper between combo and field controller. Co-Authored-By: Claude Sonnet 5.5 --- .../FrmEntriesAJAXSubmitController.php | 14 +++++++- classes/controllers/FrmFieldsController.php | 9 +++--- classes/helpers/FrmFieldsHelper.php | 22 +++++++++++++ classes/models/fields/FrmFieldCombo.php | 32 ++++++++++++------- css/_single_theme.css.php | 13 +++++++- tests/phpunit/fields/test_FrmFieldName.php | 23 +++++++++++++ 6 files changed, 94 insertions(+), 19 deletions(-) diff --git a/classes/controllers/FrmEntriesAJAXSubmitController.php b/classes/controllers/FrmEntriesAJAXSubmitController.php index 3ce89a3f18..028cb836a2 100644 --- a/classes/controllers/FrmEntriesAJAXSubmitController.php +++ b/classes/controllers/FrmEntriesAJAXSubmitController.php @@ -171,6 +171,18 @@ private static function print_ajax_scripts() { * @return string */ private static function maybe_modify_ajax_error( $error, $field_id, $form, $errors ) { + $sub_field = ''; + + if ( ! is_numeric( $field_id ) && preg_match( '/^(\d+)-(\w+)$/', $field_id, $matches ) ) { + // An error for a combo sub field, like 12-first. An empty one only flags the sub field. + if ( '' === $error ) { + return $error; + } + + $field_id = $matches[1]; + $sub_field = $matches[2]; + } + if ( ! is_numeric( $field_id ) ) { return $error; } @@ -186,7 +198,7 @@ private static function maybe_modify_ajax_error( $error, $field_id, $form, $erro if ( false !== $error_body ) { $error = str_replace( '[error]', $error, $error_body ); - $error = str_replace( '[key]', $use_field['field_key'], $error ); + $error = str_replace( '[key]', $use_field['field_key'] . ( $sub_field ? '_' . $sub_field : '' ), $error ); } return $error; diff --git a/classes/controllers/FrmFieldsController.php b/classes/controllers/FrmFieldsController.php index 0d9f181b32..44aff66db0 100644 --- a/classes/controllers/FrmFieldsController.php +++ b/classes/controllers/FrmFieldsController.php @@ -963,14 +963,13 @@ private static function add_validation_messages( $field, array &$add_html ) { $field_validation_messages_status = self::get_validation_data_attribute_visibility_info( $field ); if ( FrmField::is_required( $field ) && ! empty( $field_validation_messages_status['data-reqmsg'] ) ) { - $required_field = $field; - - if ( ! empty( $field['subfield_label'] ) ) { + if ( empty( $field['subfield_label'] ) ) { + $required_message = FrmFieldsHelper::get_error_msg( $field, 'blank' ); + } else { // A combo sub field, like a first name, is named on its own in its required message. - $required_field['name'] = $field['subfield_label']; + $required_message = FrmFieldsHelper::get_error_msg_for_name( $field, 'blank', $field['subfield_label'] ); } - $required_message = FrmFieldsHelper::get_error_msg( $required_field, 'blank' ); $add_html['data-reqmsg'] = 'data-reqmsg="' . esc_attr( $required_message ) . '"'; self::maybe_add_html_required( $field, $add_html ); } diff --git a/classes/helpers/FrmFieldsHelper.php b/classes/helpers/FrmFieldsHelper.php index 52678aebaf..2628023583 100644 --- a/classes/helpers/FrmFieldsHelper.php +++ b/classes/helpers/FrmFieldsHelper.php @@ -446,6 +446,28 @@ public static function fill_field( &$values, $field, $form_id, $new_key = '' ) { } } + /** + * Gets an error message with a different name in place of the field name. + * + * @since x.x + * + * @param array|object $field + * @param string $error + * @param string $name The name to use in the message. + * + * @return string + */ + public static function get_error_msg_for_name( $field, $error, $name ) { + if ( is_object( $field ) ) { + $field = clone $field; + $field->name = $name; + } elseif ( is_array( $field ) ) { + $field['name'] = $name; + } + + return self::get_error_msg( $field, $error ); + } + /** * @since 2.0 * diff --git a/classes/models/fields/FrmFieldCombo.php b/classes/models/fields/FrmFieldCombo.php index d012a17180..eb2774e7c4 100644 --- a/classes/models/fields/FrmFieldCombo.php +++ b/classes/models/fields/FrmFieldCombo.php @@ -480,8 +480,8 @@ public function validate( $args ) { /** * Gets the label a sub field is referred to by in its error messages. * - * The sub field description is used when there is one, since that is the label shown under - * the input. Otherwise the sub field label is combined with the field label, like "Address Line 1". + * The sub field description is used when it is a short phrase, since that is the label shown + * under the input. Otherwise the sub field label is combined with the field label, like "Address Line 1". * * @since x.x * @@ -492,7 +492,7 @@ public function validate( $args ) { public function get_sub_field_label( $name ) { $desc = FrmField::get_option( $this->field, $name . '_desc' ); - if ( is_string( $desc ) && '' !== trim( $desc ) ) { + if ( is_string( $desc ) && $this->is_short_phrase( $desc ) ) { return $desc; } @@ -506,6 +506,22 @@ public function get_sub_field_label( $name ) { return sprintf( __( '%1$s %2$s', 'formidable' ), $this->get_field_column( 'name' ), $label ); } + /** + * Checks if text reads as a label, so it can be the subject of an error message. A sentence + * would not. + * + * @since x.x + * + * @param string $text + * + * @return bool + */ + private function is_short_phrase( $text ) { + $text = trim( $text ); + + return '' !== $text && count( preg_split( '/\s+/', $text ) ) <= 4 && ! preg_match( '/[.!?:;]$/', $text ); + } + /** * Gets an error message for a single sub field, with the sub field label in place of the * field label. @@ -518,15 +534,7 @@ public function get_sub_field_label( $name ) { * @return string */ public function get_sub_field_error_msg( $name, $error ) { - $field = is_object( $this->field ) ? clone $this->field : $this->field; - - if ( is_object( $field ) ) { - $field->name = $this->get_sub_field_label( $name ); - } elseif ( is_array( $field ) ) { - $field['name'] = $this->get_sub_field_label( $name ); - } - - return FrmFieldsHelper::get_error_msg( $field, $error ); + return FrmFieldsHelper::get_error_msg_for_name( $this->field, $error, $this->get_sub_field_label( $name ) ); } /** diff --git a/css/_single_theme.css.php b/css/_single_theme.css.php index 7ac1f34186..4aa58e84fd 100644 --- a/css/_single_theme.css.php +++ b/css/_single_theme.css.php @@ -393,7 +393,18 @@ /* A combo field error only marks the sub fields that failed, not optional or filled ones. */ . .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) input:not(:focus), -. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) select:not(:focus) { +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) select:not(:focus), +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) textarea:not(:focus), + +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) .mce-edit-area iframe, + + +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) .chosen-container-multi .chosen-choices, +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) .chosen-container-single .chosen-single, + +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) .frm-g-recaptcha iframe, +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) .g-recaptcha iframe, +. .frm_blank_field .frm_combo_inputs_container > .frm_form_field:not(.frm_blank_field) .frm-card-element.StripeElement { color:var(--text-color); background-color:var(--bg-color); border-color:var(--border-color); diff --git a/tests/phpunit/fields/test_FrmFieldName.php b/tests/phpunit/fields/test_FrmFieldName.php index 5cb5072a4e..8bc00a78d5 100644 --- a/tests/phpunit/fields/test_FrmFieldName.php +++ b/tests/phpunit/fields/test_FrmFieldName.php @@ -69,4 +69,27 @@ public function test_validate_required_sub_field_errors() { $this->assertSame( '', $errors[ $error_key . '-first' ] ); $this->assertSame( '', $errors[ $error_key . '-last' ] ); } + + /** + * A sub field description that reads as a sentence is not used to name the sub field. + * + * @covers FrmFieldCombo::get_sub_field_label + */ + public function test_get_sub_field_label_ignores_sentence_description() { + $field = $this->factory->field->create_and_get( + array( + 'type' => 'name', + 'form_id' => 1, + 'field_options' => array( + 'first_desc' => 'We use this to personalize your emails.', + 'last_desc' => 'Surname', + ), + ) + ); + + $name_field = new FrmFieldName( $field ); + + $this->assertSame( 'Surname', $name_field->get_sub_field_label( 'last' ) ); + $this->assertStringNotContainsString( 'personalize', $name_field->get_sub_field_label( 'first' ) ); + } } From c21df3f88b3f6661f220a61ab8236371acdd152b Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 06:31:45 -0600 Subject: [PATCH 5/9] Avoid for...of in the error container lookup Co-Authored-By: Claude Sonnet 5.5 --- js/formidable.js | 10 +++------- js/formidable.min.js | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/js/formidable.js b/js/formidable.js index 410934d3d0..de9b9ecce7 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -385,13 +385,9 @@ function frmFrontFormJS() { } // A sub field container in a repeater does not have an ID that matches its key, so look it up from its input. - for ( const comboContainer of form.querySelectorAll( '.frm_combo_inputs_container' ) ) { - const input = getRequiredComboSubInputs( comboContainer ).find( subInput => key === getFieldId( subInput, true ) ); - if ( input ) { - return input.closest( '.frm_form_field' ); - } - } - return null; + const inputs = Array.from( form.querySelectorAll( '.frm_combo_inputs_container' ) ).flatMap( getRequiredComboSubInputs ); + const input = inputs.find( subInput => key === getFieldId( subInput, true ) ); + return input ? input.closest( '.frm_form_field' ) : null; } /** diff --git a/js/formidable.min.js b/js/formidable.min.js index 8ec138d759..18a428a917 100644 --- a/js/formidable.min.js +++ b/js/formidable.min.js @@ -7,17 +7,17 @@ null;if(!isVisible)return;requiredField.querySelectorAll("input, select, textare field.type)checkValidity(field,errors);const isConfirmationField=field.name&&0===field.name.indexOf("item_meta[conf_");if(!isConfirmationField)return}validateFieldValue(field,errors,true);checkValidity(field,errors)});if(!hasInvisibleRecaptcha(object))errors=validateRecaptcha(object,errors);return errors}function checkValidity(field,errors){if("object"!==typeof field.validity||false!==field.validity.valid)return;const fieldID=getFieldId(field,true);if(errors[fieldID]===undefined)errors[fieldID]=getFieldValidationMessage(field, "data-invmsg");if("function"===typeof field.reportValidity)field.reportValidity()}function hasClass(element,targetClass){return element.classList&&element.classList.contains(targetClass)}function maybeValidateChange(field){if(field.type==="url")maybeAddHttpsToUrl(field);const form=field.closest("form");if(!form)return;validateField(field,hasClass(form,"frm_js_validate"))}function maybeAddHttpsToUrl(field){const url=field.value;const matches=url.match(/^(https?|ftps?|mailto|news|feed|telnet):/);if(field.value!== ""&&matches===null)field.value=`https://${url}`}function validateField(field,addErrors=true){let errors;let key;errors=[];const fieldContainer=field.closest(".frm_form_field");if(!fieldContainer)return;const comboContainer=field.closest(".frm_combo_inputs_container");if(comboContainer&&(isRequiredComboField(comboContainer)||comboFieldHasFieldError(comboContainer))){validateComboField(comboContainer,field,addErrors);return}if(hasClass(fieldContainer,"frm_required_field")&&!hasClass(field,"frm_optional"))errors= -checkRequiredField(field,errors);if(errors.length<1)validateFieldValue(field,errors,false);const hasErrors=Object.keys(errors).length>0;if(addErrors){removeFieldError(fieldContainer);if(hasErrors)for(key in errors)addFieldError(fieldContainer,key,errors)}else if(!hasErrors)removeFieldError(fieldContainer)}function getFieldContainerForErrorKey(form,key){const container=form.querySelector(`#frm_field_${key}_container`);if(container)return container;for(const comboContainer of form.querySelectorAll(".frm_combo_inputs_container")){const input= -getRequiredComboSubInputs(comboContainer).find(subInput=>key===getFieldId(subInput,true));if(input)return input.closest(".frm_form_field")}return null}function getRequiredComboSubInputs(comboContainer){return Array.from(comboContainer.querySelectorAll("input, select, textarea")).filter(input=>"hidden"!==input.type&&!hasClass(input,"frm_optional")&&null!==input.offsetParent)}function getFieldContainerErrorKey(fieldContainer){return fieldContainer.id.replace(/^frm_field_/,"").replace(/_container$/, -"")}function maybeCombineComboFieldErrors(comboContainer,errors,inputs=getRequiredComboSubInputs(comboContainer)){const fieldContainer=comboContainer.closest(".frm_form_field");if(!isRequiredComboField(comboContainer))return;const allEmpty=inputs.length&&inputs.every(input=>getFieldId(input,true)in errors);if(!allEmpty)return;const fieldKey=getFieldContainerErrorKey(fieldContainer);let message=getComboFieldRequiredMessage(comboContainer,inputs[0]);inputs.forEach(input=>{const key=getFieldId(input, -true);if(""===message&&errors[key])message=errors[key];errors[key]=""});errors[fieldKey]=message}function getComboFieldRequiredMessage(comboContainer,subInput){const message=comboContainer.getAttribute("data-reqmsg");if(!message)return"";return wrapErrorHtml(message,subInput,getComboFieldErrorKey(subInput))}function getComboFieldErrorKey(subInput){const subFieldContainer=subInput.closest("[data-sub-field-name]");const subFieldName=subFieldContainer?subFieldContainer.getAttribute("data-sub-field-name"): -"";let errorKey=subInput.id.replace(/^field_/,"");if(subFieldName&&errorKey.endsWith(`_${subFieldName}`))errorKey=errorKey.slice(0,-(subFieldName.length+1));return errorKey}function isRequiredComboField(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");return null!==fieldContainer&&hasClass(fieldContainer,"frm_required_field")}function comboFieldHasFieldError(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");if(fieldContainer&&hasClass(fieldContainer, -"frm_blank_field")&&null!==fieldContainer.querySelector(":scope > .frm_error"))return true;return null!==comboContainer.querySelector(".frm_form_field.frm_blank_field")}function validateComboField(comboContainer,field,addErrors){const fieldContainer=comboContainer.closest(".frm_form_field");const hadError=comboFieldHasFieldError(comboContainer);const inputs=getRequiredComboSubInputs(comboContainer);let errors={};if(isRequiredComboField(comboContainer)){inputs.forEach(input=>{errors=checkRequiredField(input, -errors)});maybeCombineComboFieldErrors(comboContainer,errors,inputs)}if(!(getFieldId(field,true)in errors))validateFieldValue(field,errors,false);const fieldKey=getFieldContainerErrorKey(fieldContainer);const hasErrors=Object.keys(errors).length>0;if(!addErrors&&hasErrors){const subFieldContainer=field.closest(".frm_form_field");if(subFieldContainer!==fieldContainer&&!(fieldKey in errors)&&!(getFieldId(field,true)in errors))removeFieldError(subFieldContainer);if(!(fieldKey in errors)&&hasClass(fieldContainer, -"frm_blank_field")){const failing=inputs.filter(input=>getFieldId(input,true)in errors);removeComboFieldErrors(comboContainer);failing.forEach(input=>{input.closest(".frm_form_field").classList.add("frm_blank_field");input.setAttribute("aria-invalid","true")})}return}removeComboFieldErrors(comboContainer);if(!addErrors)return;const subFieldContainers={};inputs.forEach(input=>{subFieldContainers[getFieldId(input,true)]=input.closest(".frm_form_field")});const changedKey=getFieldId(field,true);const keys= -Object.keys(errors).filter(key=>hadError||key===changedKey||key===fieldKey);const form=fieldContainer.closest("form");keys.forEach(key=>{const container=key===fieldKey?fieldContainer:subFieldContainers[key]||form&&getFieldContainerForErrorKey(form,key);if(container)addFieldError(container,key,errors)})}function removeComboFieldErrors(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");comboContainer.querySelectorAll(".frm_form_field").forEach(removeFieldError);removeFieldError(fieldContainer); -comboContainer.querySelectorAll('[aria-invalid="true"]').forEach(input=>input.setAttribute("aria-invalid","false"))}function validateFieldValue(field,errors,onSubmit){if(field.type==="hidden");else if(field.type==="number")checkNumberField(field,errors);else if(field.type==="email")checkEmailField(field,errors,onSubmit);else if(field.type==="password")checkPasswordField(field,errors,onSubmit);else if(field.type==="url")checkUrlField(field,errors);else if(field.pattern!==null)checkPatternField(field, -errors);if("tel"===field.type&&shouldCheckConfirmField(field,onSubmit))confirmField(field,errors);triggerCustomEvent(document,"frm_validate_field_value",{field,errors,onSubmit})}function checkRequiredField(field,errors){let tempVal;let i;let placeholder;let val="";let fieldID="";let fileID=field.getAttribute("data-frmfile");if(field.type==="hidden"&&fileID===null&&!isAppointmentField(field)&&!isInlineDatepickerField(field))return errors;if(field.type==="checkbox"||field.type==="radio")document.querySelectorAll(`input[name="${field.name}"]`).forEach(function(input){const requiredField= +checkRequiredField(field,errors);if(errors.length<1)validateFieldValue(field,errors,false);const hasErrors=Object.keys(errors).length>0;if(addErrors){removeFieldError(fieldContainer);if(hasErrors)for(key in errors)addFieldError(fieldContainer,key,errors)}else if(!hasErrors)removeFieldError(fieldContainer)}function getFieldContainerForErrorKey(form,key){const container=form.querySelector(`#frm_field_${key}_container`);if(container)return container;const inputs=Array.from(form.querySelectorAll(".frm_combo_inputs_container")).flatMap(getRequiredComboSubInputs); +const input=inputs.find(subInput=>key===getFieldId(subInput,true));return input?input.closest(".frm_form_field"):null}function getRequiredComboSubInputs(comboContainer){return Array.from(comboContainer.querySelectorAll("input, select, textarea")).filter(input=>"hidden"!==input.type&&!hasClass(input,"frm_optional")&&null!==input.offsetParent)}function getFieldContainerErrorKey(fieldContainer){return fieldContainer.id.replace(/^frm_field_/,"").replace(/_container$/,"")}function maybeCombineComboFieldErrors(comboContainer, +errors,inputs=getRequiredComboSubInputs(comboContainer)){const fieldContainer=comboContainer.closest(".frm_form_field");if(!isRequiredComboField(comboContainer))return;const allEmpty=inputs.length&&inputs.every(input=>getFieldId(input,true)in errors);if(!allEmpty)return;const fieldKey=getFieldContainerErrorKey(fieldContainer);let message=getComboFieldRequiredMessage(comboContainer,inputs[0]);inputs.forEach(input=>{const key=getFieldId(input,true);if(""===message&&errors[key])message=errors[key];errors[key]= +""});errors[fieldKey]=message}function getComboFieldRequiredMessage(comboContainer,subInput){const message=comboContainer.getAttribute("data-reqmsg");if(!message)return"";return wrapErrorHtml(message,subInput,getComboFieldErrorKey(subInput))}function getComboFieldErrorKey(subInput){const subFieldContainer=subInput.closest("[data-sub-field-name]");const subFieldName=subFieldContainer?subFieldContainer.getAttribute("data-sub-field-name"):"";let errorKey=subInput.id.replace(/^field_/,"");if(subFieldName&& +errorKey.endsWith(`_${subFieldName}`))errorKey=errorKey.slice(0,-(subFieldName.length+1));return errorKey}function isRequiredComboField(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");return null!==fieldContainer&&hasClass(fieldContainer,"frm_required_field")}function comboFieldHasFieldError(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");if(fieldContainer&&hasClass(fieldContainer,"frm_blank_field")&&null!==fieldContainer.querySelector(":scope > .frm_error"))return true; +return null!==comboContainer.querySelector(".frm_form_field.frm_blank_field")}function validateComboField(comboContainer,field,addErrors){const fieldContainer=comboContainer.closest(".frm_form_field");const hadError=comboFieldHasFieldError(comboContainer);const inputs=getRequiredComboSubInputs(comboContainer);let errors={};if(isRequiredComboField(comboContainer)){inputs.forEach(input=>{errors=checkRequiredField(input,errors)});maybeCombineComboFieldErrors(comboContainer,errors,inputs)}if(!(getFieldId(field, +true)in errors))validateFieldValue(field,errors,false);const fieldKey=getFieldContainerErrorKey(fieldContainer);const hasErrors=Object.keys(errors).length>0;if(!addErrors&&hasErrors){const subFieldContainer=field.closest(".frm_form_field");if(subFieldContainer!==fieldContainer&&!(fieldKey in errors)&&!(getFieldId(field,true)in errors))removeFieldError(subFieldContainer);if(!(fieldKey in errors)&&hasClass(fieldContainer,"frm_blank_field")){const failing=inputs.filter(input=>getFieldId(input,true)in +errors);removeComboFieldErrors(comboContainer);failing.forEach(input=>{input.closest(".frm_form_field").classList.add("frm_blank_field");input.setAttribute("aria-invalid","true")})}return}removeComboFieldErrors(comboContainer);if(!addErrors)return;const subFieldContainers={};inputs.forEach(input=>{subFieldContainers[getFieldId(input,true)]=input.closest(".frm_form_field")});const changedKey=getFieldId(field,true);const keys=Object.keys(errors).filter(key=>hadError||key===changedKey||key===fieldKey); +const form=fieldContainer.closest("form");keys.forEach(key=>{const container=key===fieldKey?fieldContainer:subFieldContainers[key]||form&&getFieldContainerForErrorKey(form,key);if(container)addFieldError(container,key,errors)})}function removeComboFieldErrors(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");comboContainer.querySelectorAll(".frm_form_field").forEach(removeFieldError);removeFieldError(fieldContainer);comboContainer.querySelectorAll('[aria-invalid="true"]').forEach(input=> +input.setAttribute("aria-invalid","false"))}function validateFieldValue(field,errors,onSubmit){if(field.type==="hidden");else if(field.type==="number")checkNumberField(field,errors);else if(field.type==="email")checkEmailField(field,errors,onSubmit);else if(field.type==="password")checkPasswordField(field,errors,onSubmit);else if(field.type==="url")checkUrlField(field,errors);else if(field.pattern!==null)checkPatternField(field,errors);if("tel"===field.type&&shouldCheckConfirmField(field,onSubmit))confirmField(field, +errors);triggerCustomEvent(document,"frm_validate_field_value",{field,errors,onSubmit})}function checkRequiredField(field,errors){let tempVal;let i;let placeholder;let val="";let fieldID="";let fileID=field.getAttribute("data-frmfile");if(field.type==="hidden"&&fileID===null&&!isAppointmentField(field)&&!isInlineDatepickerField(field))return errors;if(field.type==="checkbox"||field.type==="radio")document.querySelectorAll(`input[name="${field.name}"]`).forEach(function(input){const requiredField= input.closest(".frm_required_field");if(!requiredField)return;const checkedInputs=requiredField.querySelectorAll("input:checked");checkedInputs.forEach(function(checkedInput){val=checkedInput.value})});else if(field.type==="file"||fileID){if(fileID===undefined){fileID=getFieldId(field,true);fileID=fileID.replace("file","")}if(errors[fileID]===undefined)val=getFileVals(fileID);fieldID=fileID}else{if(hasClass(field,"frm_pos_none"))return errors;val=jQuery(field).val();if(val===null)val="";else if(typeof val!== "string"){tempVal=val;val="";for(i=0;i Date: Wed, 30 Sep 2026 06:35:20 -0600 Subject: [PATCH 6/9] Take the aria-describedby ID from the rendered error HTML The ID is computed once by the custom error HTML, so sub field and combined errors both point at the element that is actually rendered. Co-Authored-By: Claude Sonnet 5.5 --- js/formidable.js | 5 +++-- js/formidable.min.js | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/js/formidable.js b/js/formidable.js index de9b9ecce7..f09be74fcd 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -1495,8 +1495,9 @@ function frmFrontFormJS() { container.classList.add( 'frm_blank_field' ); const allInputs = container.querySelectorAll( 'input, select, textarea' ); - const isComboField = key === getFieldContainerErrorKey( container ) && null !== container.querySelector( '.frm_combo_inputs_container' ); - const id = isComboField ? `frm_error_field_${ getComboFieldErrorKey( allInputs[ 0 ] ) }` : getErrorElementId( key, allInputs[ 0 ] ); + // Custom error HTML carries its own ID, so use that one for aria-describedby. + const htmlId = 'string' === typeof jsErrors[ key ] && jsErrors[ key ].includes( ' ! hasClass( input, 'frm_optional' ) || input.closest( '.frm_form_field' ) === container diff --git a/js/formidable.min.js b/js/formidable.min.js index 18a428a917..954580bc26 100644 --- a/js/formidable.min.js +++ b/js/formidable.min.js @@ -46,11 +46,11 @@ if("string"===typeof action&&action.includes("?action=frm_forms_preview"))ajaxUr endReplace.forEach(el=>el.remove())}}function maybeSlideOut(oldContent,newContent){let c;let newClass="frm_slideout";if(newContent.includes(" frm_slide")){c=oldContent.children();if(newContent.includes(" frm_going_back"))newClass+=" frm_going_back";c.removeClass("frm_going_back");c.addClass(newClass);return 300}return 0}function addUrlParam(response){let url;if(history.pushState&&response.page!==undefined){url=addQueryVar("frm_page",response.page);window.history.pushState({html:response.html},"", `?${url}`)}}function addQueryVar(key,value){key=encodeURI(key);value=encodeURI(value);const kvp=document.location.search.substr(1).split("&");let i=kvp.length;while(i--){const x=kvp[i].split("=");if(x[0]==key){x[1]=value;kvp[i]=x.join("=");break}}if(i<0)kvp[kvp.length]=[key,value].join("=");return kvp.join("&")}function getErrorConfigForForm(formEl){const fallback={includeAlertRole:!!frm_js.include_alert_role,focusFirstError:!!frm_js.focus_first_error,focusErrorSummary:false};if(!formEl||!formEl.dataset.frmErrorConfig)return fallback; if(!formEl.frmErrorConfigCache)try{formEl.frmErrorConfigCache=JSON.parse(formEl.dataset.frmErrorConfig)}catch(e){formEl.frmErrorConfigCache=fallback}return formEl.frmErrorConfigCache}function insertErrorHtml(container,errorHtml){const template=document.createElement("template");template.innerHTML=errorHtml;Array.from(template.content.children).forEach(el=>el.setAttribute("data-frm-error",""));container.append(template.content)}function addFieldError($fieldCont,key,jsErrors){const container=$fieldCont instanceof -jQuery?$fieldCont.get(0):$fieldCont;if(!container||container.offsetParent===null)return;container.classList.add("frm_blank_field");const allInputs=container.querySelectorAll("input, select, textarea");const isComboField=key===getFieldContainerErrorKey(container)&&null!==container.querySelector(".frm_combo_inputs_container");const id=isComboField?`frm_error_field_${getComboFieldErrorKey(allInputs[0])}`:getErrorElementId(key,allInputs[0]);const inputs=Array.from(allInputs).filter(input=>!hasClass(input, -"frm_optional")||input.closest(".frm_form_field")===container);let describedBy;if(typeof frmThemeOverride_frmPlaceError==="function")frmThemeOverride_frmPlaceError(key,jsErrors);else if(""===jsErrors[key]);else{let errorHtml;if(jsErrors[key].includes("${jsErrors[key]}`}insertErrorHtml(container, -errorHtml);inputs.forEach(input=>{describedBy=input.getAttribute("aria-describedby");if(!describedBy)describedBy=id;else if(!describedBy.includes(id)&&!describedBy.includes("frm_error_field_")){const {errorFirst}=input.dataset;if(errorFirst==="0")describedBy=`${describedBy} ${id}`;else describedBy=`${id} ${describedBy}`}input.setAttribute("aria-describedby",describedBy)})}inputs.forEach(input=>{if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]'); -if(group)group.setAttribute("aria-invalid","true")}else input.setAttribute("aria-invalid","true")});jQuery(document).trigger("frmAddFieldError",[jQuery(container),key,jsErrors])}function getErrorElementId(key,input){if(isNaN(key)||!input||!input.id)return`frm_error_field_${key}`;return`frm_error_${input.id}`}function removeFieldError(fieldCont){const container=fieldCont instanceof jQuery?fieldCont.get(0):fieldCont;if(!container)return;const errorMessages=container.querySelectorAll(".frm_error, [data-frm-error]"); -const input=container.querySelector("input, select, textarea");container.classList.remove("frm_blank_field","has-error");if(input)if("true"===input.getAttribute("aria-invalid"))input.setAttribute("aria-invalid","false");else if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","false")}errorMessages.forEach(errorMessage=>{removeElementFromInputDescribedBy(errorMessage);errorMessage.remove()})}function removeElementFromInputDescribedBy(el){document.querySelectorAll(`[aria-describedby*="${el.id}"]`).forEach(input=> +jQuery?$fieldCont.get(0):$fieldCont;if(!container||container.offsetParent===null)return;container.classList.add("frm_blank_field");const allInputs=container.querySelectorAll("input, select, textarea");const htmlId="string"===typeof jsErrors[key]&&jsErrors[key].includes("!hasClass(input,"frm_optional")||input.closest(".frm_form_field")===container); +let describedBy;if(typeof frmThemeOverride_frmPlaceError==="function")frmThemeOverride_frmPlaceError(key,jsErrors);else if(""===jsErrors[key]);else{let errorHtml;if(jsErrors[key].includes("${jsErrors[key]}`}insertErrorHtml(container,errorHtml);inputs.forEach(input=>{describedBy= +input.getAttribute("aria-describedby");if(!describedBy)describedBy=id;else if(!describedBy.includes(id)&&!describedBy.includes("frm_error_field_")){const {errorFirst}=input.dataset;if(errorFirst==="0")describedBy=`${describedBy} ${id}`;else describedBy=`${id} ${describedBy}`}input.setAttribute("aria-describedby",describedBy)})}inputs.forEach(input=>{if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid", +"true")}else input.setAttribute("aria-invalid","true")});jQuery(document).trigger("frmAddFieldError",[jQuery(container),key,jsErrors])}function getErrorElementId(key,input){if(isNaN(key)||!input||!input.id)return`frm_error_field_${key}`;return`frm_error_${input.id}`}function removeFieldError(fieldCont){const container=fieldCont instanceof jQuery?fieldCont.get(0):fieldCont;if(!container)return;const errorMessages=container.querySelectorAll(".frm_error, [data-frm-error]");const input=container.querySelector("input, select, textarea"); +container.classList.remove("frm_blank_field","has-error");if(input)if("true"===input.getAttribute("aria-invalid"))input.setAttribute("aria-invalid","false");else if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","false")}errorMessages.forEach(errorMessage=>{removeElementFromInputDescribedBy(errorMessage);errorMessage.remove()})}function removeElementFromInputDescribedBy(el){document.querySelectorAll(`[aria-describedby*="${el.id}"]`).forEach(input=> {let ariaDescribedBy=input.getAttribute("aria-describedby").split(" ");ariaDescribedBy=ariaDescribedBy.filter(value=>{const trimmedValue=value.trim();return trimmedValue&&trimmedValue!==el.id});if(ariaDescribedBy.length){input.setAttribute("aria-describedby",ariaDescribedBy.join(" "));return}input.removeAttribute("aria-describedby")})}function removeAllErrors(){document.querySelectorAll(".form-field").forEach(field=>{field.classList.remove("frm_blank_field","has-error")});document.querySelectorAll(".form-field .frm_error, .form-field [data-frm-error]").forEach(el=> {removeElementFromInputDescribedBy(el);el.remove()});document.querySelectorAll(".frm_error_style").forEach(error=>error.remove())}function scrollToFirstField(object){if("function"===typeof object.get)object=object.get(0);const field=object.querySelector(".frm_blank_field");if(field)frmFrontForm.scrollMsg(jQuery(field),object,true)}function showSubmitLoading($object){showLoadingIndicator($object);disableSubmitButton($object);disableSaveDraft($object)}function showLoadingIndicator($object){if(!$object.hasClass("frm_loading_form")&& !$object.hasClass("frm_loading_prev")){addLoadingClass($object);$object.trigger("frmStartFormLoading")}}function addLoadingClass($object){const loadingClass=isGoingToPrevPage($object)?"frm_loading_prev":"frm_loading_form";$object.addClass(loadingClass)}function isGoingToPrevPage($object){return typeof frmProForm!=="undefined"&&frmProForm.goingToPreviousPage($object)}function removeSubmitLoading(_,enable,processesRunning){if(processesRunning>0)return;document.querySelectorAll(".frm_loading_form").forEach(function(form){form.classList.remove("frm_loading_form", From f82ae8f5ff55078b222c21089072bc226f131c85 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 06:35:26 -0600 Subject: [PATCH 7/9] Tighten combo sub field key match and word count Co-Authored-By: Claude Sonnet 5.5 --- classes/controllers/FrmEntriesAJAXSubmitController.php | 2 +- classes/models/fields/FrmFieldCombo.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/controllers/FrmEntriesAJAXSubmitController.php b/classes/controllers/FrmEntriesAJAXSubmitController.php index 028cb836a2..c938e27d19 100644 --- a/classes/controllers/FrmEntriesAJAXSubmitController.php +++ b/classes/controllers/FrmEntriesAJAXSubmitController.php @@ -173,7 +173,7 @@ private static function print_ajax_scripts() { private static function maybe_modify_ajax_error( $error, $field_id, $form, $errors ) { $sub_field = ''; - if ( ! is_numeric( $field_id ) && preg_match( '/^(\d+)-(\w+)$/', $field_id, $matches ) ) { + if ( ! is_numeric( $field_id ) && preg_match( '/^(\d+)-([a-z]\w*)$/', $field_id, $matches ) ) { // An error for a combo sub field, like 12-first. An empty one only flags the sub field. if ( '' === $error ) { return $error; diff --git a/classes/models/fields/FrmFieldCombo.php b/classes/models/fields/FrmFieldCombo.php index eb2774e7c4..08df514793 100644 --- a/classes/models/fields/FrmFieldCombo.php +++ b/classes/models/fields/FrmFieldCombo.php @@ -519,7 +519,7 @@ public function get_sub_field_label( $name ) { private function is_short_phrase( $text ) { $text = trim( $text ); - return '' !== $text && count( preg_split( '/\s+/', $text ) ) <= 4 && ! preg_match( '/[.!?:;]$/', $text ); + return '' !== $text && count( (array) preg_split( '/\s+/', $text ) ) <= 4 && ! preg_match( '/[.!?:;]$/', $text ); } /** From 7a3abd0b88f847d928228fb8e8cd41f33f0eeb03 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 06:38:18 -0600 Subject: [PATCH 8/9] Fix lint: JSDoc alignment and short function blank line Co-Authored-By: Claude Sonnet 5.5 --- classes/models/fields/FrmFieldCombo.php | 1 - js/formidable.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/classes/models/fields/FrmFieldCombo.php b/classes/models/fields/FrmFieldCombo.php index 08df514793..8acfa19151 100644 --- a/classes/models/fields/FrmFieldCombo.php +++ b/classes/models/fields/FrmFieldCombo.php @@ -518,7 +518,6 @@ public function get_sub_field_label( $name ) { */ private function is_short_phrase( $text ) { $text = trim( $text ); - return '' !== $text && count( (array) preg_split( '/\s+/', $text ) ) <= 4 && ! preg_match( '/[.!?:;]$/', $text ); } diff --git a/js/formidable.js b/js/formidable.js index f09be74fcd..5bf4997a0c 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -422,9 +422,9 @@ function frmFrontFormJS() { * * @since x.x * - * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. - * @param {Object} errors Errors keyed by field container key, including the required errors of the sub fields. Updated in place. - * @param {HTMLElement[]} inputs Optional. The required sub inputs. Defaults to a query of the combo field. + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @param {Object} errors Errors keyed by field container key, including the required errors of the sub fields. Updated in place. + * @param {HTMLElement[]} inputs Optional. The required sub inputs. Defaults to a query of the combo field. * @return {void} */ function maybeCombineComboFieldErrors( comboContainer, errors, inputs = getRequiredComboSubInputs( comboContainer ) ) { From 5164e8d9792f7a425807d7643bea0789f0f8f1a1 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:28:24 -0600 Subject: [PATCH 9/9] Keep server errors on other combo sub fields when one changes removeComboFieldErrors() cleared every sub field's error, so a server-only error such as the address ZIP format error was lost when another sub field was edited. Now it clears only the error for the whole field, the changed sub field, and sub fields the client check recomputes. Co-Authored-By: Claude Sonnet 5.5 --- js/formidable.js | 39 +++++++++++++++++++++++++++++++-------- js/formidable.min.js | 15 ++++++++------- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/js/formidable.js b/js/formidable.js index 6b57092022..5f28f4e488 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -566,7 +566,7 @@ function frmFrontFormJS() { if ( ! ( fieldKey in errors ) && hasClass( fieldContainer, 'frm_blank_field' ) ) { // The error for the whole field no longer applies. Keep the flags on the sub fields that still fail. const failing = inputs.filter( input => getFieldId( input, true ) in errors ); - removeComboFieldErrors( comboContainer ); + removeComboFieldErrors( comboContainer, field, inputs, errors ); failing.forEach( input => { input.closest( '.frm_form_field' ).classList.add( 'frm_blank_field' ); input.setAttribute( 'aria-invalid', 'true' ); @@ -575,7 +575,7 @@ function frmFrontFormJS() { return; } - removeComboFieldErrors( comboContainer ); + removeComboFieldErrors( comboContainer, field, inputs, errors ); if ( ! addErrors ) { return; @@ -604,19 +604,42 @@ function frmFrontFormJS() { } /** - * Removes the error for a whole combo field and the errors for each of its sub fields. + * Removes the errors a combo field validation is about to recompute: the error for the whole + * field, the error of the sub field that changed, and the error of any sub field that still + * fails. Other sub fields keep their errors, like a server error the client cannot recompute. * * @since x.x * - * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @param {HTMLElement} comboContainer The .frm_combo_inputs_container element. + * @param {HTMLElement} field The sub field input that changed. + * @param {HTMLElement[]} inputs The required sub inputs. + * @param {Object} errors Errors keyed by field container key. * @return {void} */ - function removeComboFieldErrors( comboContainer ) { + function removeComboFieldErrors( comboContainer, field, inputs, errors ) { const fieldContainer = comboContainer.closest( '.frm_form_field' ); + const subFieldContainers = [ field.closest( '.frm_form_field' ) ]; - comboContainer.querySelectorAll( '.frm_form_field' ).forEach( removeFieldError ); - removeFieldError( fieldContainer ); - comboContainer.querySelectorAll( '[aria-invalid="true"]' ).forEach( input => input.setAttribute( 'aria-invalid', 'false' ) ); + inputs.forEach( input => { + if ( getFieldId( input, true ) in errors ) { + subFieldContainers.push( input.closest( '.frm_form_field' ) ); + } + } ); + subFieldContainers.filter( container => container && container !== fieldContainer ).forEach( removeFieldError ); + + // Only the error placed on the field itself. The sub field errors inside it are handled above. + fieldContainer.classList.remove( 'frm_blank_field', 'has-error' ); + fieldContainer.querySelectorAll( ':scope > .frm_error, :scope > [data-frm-error]' ).forEach( errorMessage => { + removeElementFromInputDescribedBy( errorMessage ); + errorMessage.remove(); + } ); + + // The error for the whole field flags every input, so unflag the ones without an error of their own. + comboContainer.querySelectorAll( '[aria-invalid="true"]' ).forEach( input => { + if ( null === input.closest( '.frm_form_field.frm_blank_field' ) ) { + input.setAttribute( 'aria-invalid', 'false' ); + } + } ); } /** diff --git a/js/formidable.min.js b/js/formidable.min.js index 0dc7d98a87..143d26fa07 100644 --- a/js/formidable.min.js +++ b/js/formidable.min.js @@ -14,13 +14,14 @@ errors,inputs=getRequiredComboSubInputs(comboContainer)){const fieldContainer=co errorKey.endsWith(`_${subFieldName}`))errorKey=errorKey.slice(0,-(subFieldName.length+1));return errorKey}function isRequiredComboField(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");return null!==fieldContainer&&hasClass(fieldContainer,"frm_required_field")}function comboFieldHasFieldError(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");if(fieldContainer&&hasClass(fieldContainer,"frm_blank_field")&&null!==fieldContainer.querySelector(":scope > .frm_error"))return true; return null!==comboContainer.querySelector(".frm_form_field.frm_blank_field")}function validateComboField(comboContainer,field,addErrors){const fieldContainer=comboContainer.closest(".frm_form_field");const hadError=comboFieldHasFieldError(comboContainer);const inputs=getRequiredComboSubInputs(comboContainer);let errors={};if(isRequiredComboField(comboContainer)){inputs.forEach(input=>{errors=checkRequiredField(input,errors)});maybeCombineComboFieldErrors(comboContainer,errors,inputs)}if(!(getFieldId(field, true)in errors))validateFieldValue(field,errors,false);const fieldKey=getFieldContainerErrorKey(fieldContainer);const hasErrors=Object.keys(errors).length>0;if(!addErrors&&hasErrors){const subFieldContainer=field.closest(".frm_form_field");if(subFieldContainer!==fieldContainer&&!(fieldKey in errors)&&!(getFieldId(field,true)in errors))removeFieldError(subFieldContainer);if(!(fieldKey in errors)&&hasClass(fieldContainer,"frm_blank_field")){const failing=inputs.filter(input=>getFieldId(input,true)in -errors);removeComboFieldErrors(comboContainer);failing.forEach(input=>{input.closest(".frm_form_field").classList.add("frm_blank_field");input.setAttribute("aria-invalid","true")})}return}removeComboFieldErrors(comboContainer);if(!addErrors)return;const subFieldContainers={};inputs.forEach(input=>{subFieldContainers[getFieldId(input,true)]=input.closest(".frm_form_field")});const changedKey=getFieldId(field,true);const keys=Object.keys(errors).filter(key=>hadError||key===changedKey||key===fieldKey); -const form=fieldContainer.closest("form");keys.forEach(key=>{const container=key===fieldKey?fieldContainer:subFieldContainers[key]||form&&getFieldContainerForErrorKey(form,key);if(container)addFieldError(container,key,errors)})}function removeComboFieldErrors(comboContainer){const fieldContainer=comboContainer.closest(".frm_form_field");comboContainer.querySelectorAll(".frm_form_field").forEach(removeFieldError);removeFieldError(fieldContainer);comboContainer.querySelectorAll('[aria-invalid="true"]').forEach(input=> -input.setAttribute("aria-invalid","false"))}function validateFieldValue(field,errors,onSubmit){if(field.type==="hidden");else if(field.type==="number")checkNumberField(field,errors);else if(field.type==="email")checkEmailField(field,errors,onSubmit);else if(field.type==="password")checkPasswordField(field,errors,onSubmit);else if(field.type==="url")checkUrlField(field,errors);else if(field.pattern!==null)checkPatternField(field,errors);if("tel"===field.type&&shouldCheckConfirmField(field,onSubmit))confirmField(field, -errors);triggerCustomEvent(document,"frm_validate_field_value",{field,errors,onSubmit})}function checkRequiredField(field,errors){let tempVal;let i;let placeholder;let val="";let fieldID="";let fileID=field.getAttribute("data-frmfile");if(field.type==="hidden"&&fileID===null&&!isAppointmentField(field)&&!isInlineDatepickerField(field))return errors;if(field.type==="checkbox"||field.type==="radio")document.querySelectorAll(`input[name="${field.name}"]`).forEach(function(input){const requiredField= -input.closest(".frm_required_field");if(!requiredField)return;const checkedInputs=requiredField.querySelectorAll("input:checked");checkedInputs.forEach(function(checkedInput){val=checkedInput.value})});else if(field.type==="file"||fileID){if(fileID===undefined){fileID=getFieldId(field,true);fileID=fileID.replace("file","")}if(errors[fileID]===undefined)val=getFileVals(fileID);fieldID=fileID}else{if(hasClass(field,"frm_pos_none"))return errors;val=jQuery(field).val();if(val===null)val="";else if(typeof val!== -"string"){tempVal=val;val="";for(i=0;i{input.closest(".frm_form_field").classList.add("frm_blank_field");input.setAttribute("aria-invalid","true")})}return}removeComboFieldErrors(comboContainer,field,inputs,errors);if(!addErrors)return;const subFieldContainers={};inputs.forEach(input=>{subFieldContainers[getFieldId(input,true)]=input.closest(".frm_form_field")});const changedKey=getFieldId(field,true);const keys=Object.keys(errors).filter(key=>hadError|| +key===changedKey||key===fieldKey);const form=fieldContainer.closest("form");keys.forEach(key=>{const container=key===fieldKey?fieldContainer:subFieldContainers[key]||form&&getFieldContainerForErrorKey(form,key);if(container)addFieldError(container,key,errors)})}function removeComboFieldErrors(comboContainer,field,inputs,errors){const fieldContainer=comboContainer.closest(".frm_form_field");const subFieldContainers=[field.closest(".frm_form_field")];inputs.forEach(input=>{if(getFieldId(input,true)in +errors)subFieldContainers.push(input.closest(".frm_form_field"))});subFieldContainers.filter(container=>container&&container!==fieldContainer).forEach(removeFieldError);fieldContainer.classList.remove("frm_blank_field","has-error");fieldContainer.querySelectorAll(":scope > .frm_error, :scope > [data-frm-error]").forEach(errorMessage=>{removeElementFromInputDescribedBy(errorMessage);errorMessage.remove()});comboContainer.querySelectorAll('[aria-invalid="true"]').forEach(input=>{if(null===input.closest(".frm_form_field.frm_blank_field"))input.setAttribute("aria-invalid", +"false")})}function validateFieldValue(field,errors,onSubmit){if(field.type==="hidden");else if(field.type==="number")checkNumberField(field,errors);else if(field.type==="email")checkEmailField(field,errors,onSubmit);else if(field.type==="password")checkPasswordField(field,errors,onSubmit);else if(field.type==="url")checkUrlField(field,errors);else if(field.pattern!==null)checkPatternField(field,errors);if("tel"===field.type&&shouldCheckConfirmField(field,onSubmit))confirmField(field,errors);triggerCustomEvent(document, +"frm_validate_field_value",{field,errors,onSubmit})}function checkRequiredField(field,errors){let tempVal;let i;let placeholder;let val="";let fieldID="";let fileID=field.getAttribute("data-frmfile");if(field.type==="hidden"&&fileID===null&&!isAppointmentField(field)&&!isInlineDatepickerField(field))return errors;if(field.type==="checkbox"||field.type==="radio")document.querySelectorAll(`input[name="${field.name}"]`).forEach(function(input){const requiredField=input.closest(".frm_required_field"); +if(!requiredField)return;const checkedInputs=requiredField.querySelectorAll("input:checked");checkedInputs.forEach(function(checkedInput){val=checkedInput.value})});else if(field.type==="file"||fileID){if(fileID===undefined){fileID=getFieldId(field,true);fileID=fileID.replace("file","")}if(errors[fileID]===undefined)val=getFileVals(fileID);fieldID=fileID}else{if(hasClass(field,"frm_pos_none"))return errors;val=jQuery(field).val();if(val===null)val="";else if(typeof val!=="string"){tempVal=val;val= +"";for(i=0;i()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/i;if(""!==field.value&&pattern.test(field.value)===false)errors[fieldID]=getFieldValidationMessage(field,"data-invmsg");