diff --git a/classes/models/FrmFieldFormHtml.php b/classes/models/FrmFieldFormHtml.php
index fbfb4003a8..19d1321ea2 100644
--- a/classes/models/FrmFieldFormHtml.php
+++ b/classes/models/FrmFieldFormHtml.php
@@ -311,9 +311,91 @@ private function replace_error_shortcode() {
$this->html = str_replace( 'role="alert"', '', $this->html );
}
+ $this->add_data_frm_error_attribute();
+
FrmShortcodeHelper::remove_inline_conditions( true, 'error', $error, $this->html );
}
+ /**
+ * Tag every top-level element in the [if error] block with a data-frm-error
+ * attribute, so js/formidable.js's removeFieldError()/removeAllErrors() can find
+ * and remove it on revalidation even when a custom field template's error markup
+ * carries no frm_error class or id (e.g. `[if error]
[error]
[/if error]`).
+ * Mirrors insertErrorHtml() tagging every top-level element client-side.
+ *
+ * @since x.x
+ *
+ * @return void
+ */
+ private function add_data_frm_error_attribute() {
+ $error_body = self::get_error_body( $this->html );
+
+ if ( ! is_string( $error_body ) || '' === trim( $error_body ) ) {
+ return;
+ }
+
+ $tagged_body = self::tag_top_level_elements( $error_body );
+
+ if ( $tagged_body === $error_body ) {
+ return;
+ }
+
+ $this->html = str_replace( '[if error]' . $error_body . '[/if error]', '[if error]' . $tagged_body . '[/if error]', $this->html );
+ }
+
+ /**
+ * Add a data-frm-error attribute to every element at the top level of an HTML
+ * fragment (direct children only, not nested descendants) by tracking open tags
+ * on a stack through a single scan, rather than a full DOM parse.
+ *
+ * @since x.x
+ *
+ * @param string $html
+ *
+ * @return string
+ */
+ private static function tag_top_level_elements( $html ) {
+ $void_elements = array( 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr' );
+ $open_tags = array();
+ $offset = 0;
+ $result = '';
+
+ while ( preg_match( '/<(\/?)([a-zA-Z][a-zA-Z0-9-]*)([^>]*?)(\/?)>/', $html, $match, PREG_OFFSET_CAPTURE, $offset ) ) {
+ $full_tag = $match[0][0];
+ $tag_start = $match[0][1];
+ $is_closing = '' !== $match[1][0];
+ $tag_name = strtolower( $match[2][0] );
+ $attrs = $match[3][0];
+ $self_close = '' !== $match[4][0] || in_array( $tag_name, $void_elements, true );
+
+ $result .= substr( $html, $offset, $tag_start - $offset );
+
+ if ( $is_closing ) {
+ if ( $open_tags ) {
+ array_pop( $open_tags );
+ }
+
+ $result .= $full_tag;
+ } elseif ( ! $open_tags ) {
+ $result .= '<' . $match[2][0] . $attrs . ' data-frm-error' . ( $self_close ? ' />' : '>' );
+
+ if ( ! $self_close ) {
+ $open_tags[] = $tag_name;
+ }
+ } else {
+ $result .= $full_tag;
+
+ if ( ! $self_close ) {
+ $open_tags[] = $tag_name;
+ }
+ }
+
+ $offset = $tag_start + strlen( $full_tag );
+ }//end while
+
+ return $result . substr( $html, $offset );
+ }
+
/**
* Pull the HTML between [if error] and [/if error] shortcodes.
*
diff --git a/js/formidable.js b/js/formidable.js
index 09e134947c..465389615e 100644
--- a/js/formidable.js
+++ b/js/formidable.js
@@ -1192,6 +1192,26 @@ function frmFrontFormJS() {
return formEl.frmErrorConfigCache;
}
+ /**
+ * Inserts error HTML into a field's container, tagging every inserted top-level
+ * element with a data-frm-error attribute. removeFieldError()/removeAllErrors() rely
+ * on that attribute (rather than the frm_error class) to find and remove the visible
+ * error element again, since a site's own custom field HTML template can render the
+ * [error] placeholder without a frm_error class or id. This only covers the visible
+ * element — aria-describedby cleanup still depends on an id, which custom markup may
+ * not have.
+ *
+ * @param {HTMLElement} container
+ * @param {string} errorHtml
+ * @return {void}
+ */
+ 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;
@@ -1216,7 +1236,7 @@ function frmFrontFormJS() {
const roleString = config.includeAlertRole ? 'role="alert"' : '';
errorHtml = `${ jsErrors[ key ] }
`;
}
- container.insertAdjacentHTML( 'beforeend', errorHtml );
+ insertErrorHtml( container, errorHtml );
inputs.forEach( input => {
describedBy = input.getAttribute( 'aria-describedby' );
if ( ! describedBy ) {
@@ -1275,7 +1295,7 @@ function frmFrontFormJS() {
return;
}
- const errorMessage = container.querySelector( '.frm_error' );
+ const errorMessages = container.querySelectorAll( '.frm_error, [data-frm-error]' );
const input = container.querySelector( 'input, select, textarea' );
container.classList.remove( 'frm_blank_field', 'has-error' );
@@ -1291,10 +1311,10 @@ function frmFrontFormJS() {
}
}
- if ( errorMessage ) {
+ errorMessages.forEach( errorMessage => {
removeElementFromInputDescribedBy( errorMessage );
errorMessage.remove();
- }
+ } );
}
/**
@@ -1325,7 +1345,7 @@ function frmFrontFormJS() {
document.querySelectorAll( '.form-field' ).forEach( field => {
field.classList.remove( 'frm_blank_field', 'has-error' );
} );
- document.querySelectorAll( '.form-field .frm_error' ).forEach( el => {
+ document.querySelectorAll( '.form-field .frm_error, .form-field [data-frm-error]' ).forEach( el => {
removeElementFromInputDescribedBy( el );
el.remove();
} );
@@ -1473,7 +1493,7 @@ function frmFrontFormJS() {
}
function checkForErrorsAndMaybeSetFocus() {
- const errors = document.querySelectorAll( '.frm_form_field .frm_error' );
+ const errors = document.querySelectorAll( '.frm_form_field .frm_error, .frm_form_field [data-frm-error]' );
if ( ! errors.length ) {
return;
}
@@ -1497,6 +1517,9 @@ function frmFrontFormJS() {
let timeoutCallback;
do {
element = element.previousSibling;
+ if ( ! element ) {
+ break;
+ }
if ( [ 'input', 'select', 'textarea' ].includes( element.nodeName.toLowerCase() ) ) {
focusInput( element );
break;
diff --git a/js/formidable.min.js b/js/formidable.min.js
index 264f898265..963b905235 100644
--- a/js/formidable.min.js
+++ b/js/formidable.min.js
@@ -35,18 +35,18 @@ object.submit()}};const error=function(){object.querySelectorAll('input[type="su
if("function"===typeof error)ajaxParams.error=error;jQuery.ajax(ajaxParams)}function afterFormSubmitted(object,response){const tempDiv=document.createElement("div");tempDiv.innerHTML=response.content;const formCompleted=tempDiv.querySelector(".frm_message");if(formCompleted)jQuery(document).trigger("frmFormComplete",[object,response]);else jQuery(document).trigger("frmPageChanged",[object,response])}function afterFormSubmittedBeforeReplace(object,response){const tempDiv=document.createElement("div");
tempDiv.innerHTML=response.content;const formCompleted=tempDiv.querySelector(".frm_message");if(formCompleted)triggerCustomEvent(document,"frmFormCompleteBeforeReplace",{object,response})}function removeAddedScripts(formContainer,formID){const endReplace=document.querySelectorAll(`.frm_end_ajax_${formID}`);if(endReplace.length){formContainer.nextUntil(`.frm_end_ajax_${formID}`).remove();endReplace.forEach(el=>el.remove())}}function maybeSlideOut(oldContent,newContent){let c;let newClass="frm_slideout";
if(newContent.includes(" frm_slide")){c=oldContent.children();if(newContent.includes(" frm_going_back"))newClass+=" frm_going_back";c.removeClass("frm_going_back");c.addClass(newClass);return 300}return 0}function addUrlParam(response){let url;if(history.pushState&&response.page!==undefined){url=addQueryVar("frm_page",response.page);window.history.pushState({html:response.html},"",`?${url}`)}}function addQueryVar(key,value){key=encodeURI(key);value=encodeURI(value);const kvp=document.location.search.substr(1).split("&");
-let i=kvp.length;while(i--){const x=kvp[i].split("=");if(x[0]==key){x[1]=value;kvp[i]=x.join("=");break}}if(i<0)kvp[kvp.length]=[key,value].join("=");return kvp.join("&")}function getErrorConfigForForm(formEl){const fallback={includeAlertRole:!!frm_js.include_alert_role,focusFirstError:!!frm_js.focus_first_error,focusErrorSummary:false};if(!formEl||!formEl.dataset.frmErrorConfig)return fallback;if(!formEl.frmErrorConfigCache){try{formEl.frmErrorConfigCache=JSON.parse(formEl.dataset.frmErrorConfig)}catch(e){formEl.frmErrorConfigCache=fallback}}return formEl.frmErrorConfigCache}function addFieldError($fieldCont,key,jsErrors){const container=$fieldCont instanceof jQuery?$fieldCont.get(0):$fieldCont;if(!container||container.offsetParent===null)return;container.classList.add("frm_blank_field");const inputs=container.querySelectorAll("input, select, textarea");const id=getErrorElementId(key,inputs[0]);let describedBy;
-if(typeof frmThemeOverride_frmPlaceError==="function")frmThemeOverride_frmPlaceError(key,jsErrors);else{let errorHtml;if(jsErrors[key].includes("${jsErrors[key]}
`}container.insertAdjacentHTML("beforeend",errorHtml);inputs.forEach(input=>{describedBy=input.getAttribute("aria-describedby");if(!describedBy)describedBy=id;else if(!describedBy.includes(id)&&
+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 errorMessage=container.querySelector(".frm_error");const input=container.querySelector("input, select, textarea");container.classList.remove("frm_blank_field","has-error");if(input)if("true"===input.getAttribute("aria-invalid"))input.setAttribute("aria-invalid",
-"false");else if(["radio","checkbox"].includes(input.type)){const group=input.closest('[role="radiogroup"], [role="group"]');if(group)group.setAttribute("aria-invalid","false")}if(errorMessage){removeElementFromInputDescribedBy(errorMessage);errorMessage.remove()}}function removeElementFromInputDescribedBy(el){document.querySelectorAll(`[aria-describedby*="${el.id}"]`).forEach(input=>{let ariaDescribedBy=input.getAttribute("aria-describedby").split(" ");ariaDescribedBy=ariaDescribedBy.filter(value=>
-{const trimmedValue=value.trim();return trimmedValue&&trimmedValue!==el.id});if(ariaDescribedBy.length){input.setAttribute("aria-describedby",ariaDescribedBy.join(" "));return}input.removeAttribute("aria-describedby")})}function removeAllErrors(){document.querySelectorAll(".form-field").forEach(field=>{field.classList.remove("frm_blank_field","has-error")});document.querySelectorAll(".form-field .frm_error").forEach(el=>{removeElementFromInputDescribedBy(el);el.remove()});document.querySelectorAll(".frm_error_style").forEach(error=>
+[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");if(!errors.length)return;const formContainer=errors[0].closest(".frm-show-form");const config=getErrorConfigForForm(formContainer);if(config.focusErrorSummary){const summary=formContainer?formContainer.querySelector("[data-frm-error-summary]"):null;if(summary){summary.focus();return}}if(!config.focusFirstError)return;let element=errors[0];let timeoutCallback;
-do{element=element.previousSibling;if(["input","select","textarea"].includes(element.nodeName.toLowerCase())){focusInput(element);break}if(maybeFocusOnComboSubField(element))break;if(element.classList!==undefined){if(element.classList.contains("html-active"))timeoutCallback=function(){const textarea=element.querySelector("textarea");if(null!==textarea)textarea.focus()};else if(element.classList.contains("tmce-active"))timeoutCallback=function(){tinyMCE.activeEditor.focus()};else if(element.classList.contains("frm_opt_container")){const firstInput=
+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)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,0);break}}}while(element.previousSibling)}function focusInput(input){if(input.offsetParent!==null)input.focus();else triggerCustomEvent(document,"frmMaybeDelayFocus",{input})}function focusFieldFromErrorLink(event){const href=this.getAttribute("href");if(!href||!href.startsWith("#"))return;const container=document.getElementById(href.substring(1));if(!container)return;
event.preventDefault();container.scrollIntoView({behavior:"smooth",block:"center"});const input=getFocusableInputInField(container);if(input){focusInput(input);return}container.setAttribute("tabindex","-1");focusInput(container)}function getFocusableInputInField(container){const inputs=Array.from(container.querySelectorAll(FOCUSABLE_FIELD_SELECTOR)).filter(inputCanTakeFocus);if(!inputs.length)return null;const invalidInput=inputs.find(input=>"true"===input.getAttribute("aria-invalid"));if(invalidInput)return invalidInput;
return inputs.find(fieldInputIsEmpty)||inputs[0]}function fieldInputIsEmpty(input){if("BUTTON"===input.nodeName||["button","checkbox","file","radio","submit"].includes(input.type))return false;return""===String(input.value||"").trim()}function inputCanTakeFocus(input){if(input.disabled||"hidden"===input.type)return false;const rect=input.getBoundingClientRect();if(!rect.width&&!rect.height)return false;return"hidden"!==getComputedStyle(input).visibility}function documentOn(event,selector,handler,