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/FrmEntriesAJAXSubmitController.php b/classes/controllers/FrmEntriesAJAXSubmitController.php
index 3ce89a3f18..c938e27d19 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+)-([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;
+ }
+
+ $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 55f2bb1282..55be85b3c3 100644
--- a/classes/controllers/FrmFieldsController.php
+++ b/classes/controllers/FrmFieldsController.php
@@ -964,7 +964,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_message = FrmFieldsHelper::get_error_msg( $field, 'blank' );
+ 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_message = FrmFieldsHelper::get_error_msg_for_name( $field, 'blank', $field['subfield_label'] );
+ }
+
$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 cca42c1582..bd590119ba 100644
--- a/classes/helpers/FrmFieldsHelper.php
+++ b/classes/helpers/FrmFieldsHelper.php
@@ -463,6 +463,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 ae3c872974..8acfa19151 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,104 @@ 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 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
+ *
+ * @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 ) && $this->is_short_phrase( $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 );
+ }
+
+ /**
+ * 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( (array) 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.
+ *
+ * @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 ) {
+ return FrmFieldsHelper::get_error_msg_for_name( $this->field, $error, $this->get_sub_field_label( $name ) );
+ }
+
/**
* Gets export headings.
*
@@ -507,10 +590,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..4aa58e84fd 100644
--- a/css/_single_theme.css.php
+++ b/css/_single_theme.css.php
@@ -391,6 +391,27 @@
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),
+. .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);
+ 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 +340,12 @@ function frmFrontFormJS() {
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 );
}
@@ -351,6 +369,279 @@ 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.
+ 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;
+ }
+
+ /**
+ * 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, 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 ) ) {
+ 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 ] ) {
+ // 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.
+ 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 '';
+ }
+
+ 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}'.
+ let errorKey = subInput.id.replace( /^field_/, '' );
+ if ( subFieldName && errorKey.endsWith( `_${ subFieldName }` ) ) {
+ errorKey = errorKey.slice( 0, -( subFieldName.length + 1 ) );
+ }
+ return 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' );
+ }
+
+ /**
+ * 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 field or one of its sub fields has an error.
+ */
+ 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;
+ }
+
+ // 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' );
+ }
+
+ /**
+ * 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' );
+ 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 ) {
+ // 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 );
+ }
+
+ 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, field, inputs, errors );
+ failing.forEach( input => {
+ input.closest( '.frm_form_field' ).classList.add( 'frm_blank_field' );
+ input.setAttribute( 'aria-invalid', 'true' );
+ } );
+ }
+ return;
+ }
+
+ removeComboFieldErrors( comboContainer, field, inputs, errors );
+
+ if ( ! addErrors ) {
+ 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 = {};
+ 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' );
+ keys.forEach( key => {
+ const container = key === fieldKey ? fieldContainer : ( subFieldContainers[ key ] || ( form && getFieldContainerForErrorKey( form, key ) ) );
+ if ( container ) {
+ addFieldError( container, key, errors );
+ }
+ } );
+ }
+
+ /**
+ * 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} 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, 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 );
+
+ // 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' );
+ }
+ } );
+ }
+
/**
* Validates a field value.
*
@@ -817,11 +1108,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;
@@ -829,6 +1123,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( '_' );
@@ -1227,13 +1524,21 @@ 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' );
+ // 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
+ );
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( '
-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 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 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]}
`}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()})})}
+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&&(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;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,field,inputs,errors);failing.forEach(input=>{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");
+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 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 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","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");
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&&!config.focusErrorSummary)return;let element=errors[0];let timeoutCallback;do{element=element.previousSibling;if(!element)break;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,
@@ -82,11 +94,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
create_address_field_type( 'international', 1 );
@@ -391,10 +391,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 20c5de736c..8bc00a78d5 100644
--- a/tests/phpunit/fields/test_FrmFieldName.php
+++ b/tests/phpunit/fields/test_FrmFieldName.php
@@ -24,4 +24,72 @@ 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' ] );
+ }
+
+ /**
+ * 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' ) );
+ }
}