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;ccreate_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' ) ); + } }