Skip to content

Stop using jQuery .find() in formidable.js#2627

Merged
Crabcyborg merged 8 commits into
masterfrom
try_to_use_less_jquery
Dec 4, 2025
Merged

Stop using jQuery .find() in formidable.js#2627
Crabcyborg merged 8 commits into
masterfrom
try_to_use_less_jquery

Conversation

@Crabcyborg
Copy link
Copy Markdown
Contributor

@Crabcyborg Crabcyborg commented Dec 3, 2025

Related issue https://github.com/Strategy11/formidable-pro/issues/5002

This could use more testing.

This update mostly touches code for:

Tested

  • A honeypot fix in Chrome
  • Form error validation (AJAX and JS)
  • reCAPTCHA validation
  • Disabling / enabling submit buttons and the save draft button
  • TinyMCE handling on form submit.
  • Required Signature field validation
  • Click confirmation (deleting entries from front end)
  • Enabling / disabling the save draft button.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 3, 2025

Walkthrough

Replaces jQuery-based DOM access and UI state handling in js/formidable.js with native DOM APIs (querySelector*/closest/classList/dataset), adds defensive null checks, updates AJAX error parsing to use DOM fragments, and adjusts JSDoc/type annotations to accept HTMLElements or jQuery objects where relevant.

Changes

Cohort / File(s) Change Summary
jQuery → Native DOM (single file)
js/formidable.js
Replaced jQuery selectors/traversals and collection iterations with native DOM APIs (querySelector, querySelectorAll, closest, loops/forEach); converted UI state updates to native properties (disabled, style, classList); added null/undefined guards; reworked AJAX error parsing using temporary DOM fragments; updated reCAPTCHA functions and JSDoc/type annotations to prefer HTMLElement while tolerating jQuery inputs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify null/undefined guards around element access (dataset, attributes, parent/closest).
  • Confirm mixed input handling (HTMLElement vs. jQuery) works across callers and event wiring.
  • Review AJAX error parsing and DOM-fragment usage for edge cases (malformed HTML).
  • Inspect reCAPTCHA functions (hasInvisibleRecaptcha, validateRecaptcha, executeInvisibleRecaptcha) for correct behavior when elements are absent.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: replacing jQuery .find() calls with native DOM APIs in formidable.js, which aligns with the AI-generated summary highlighting jQuery-to-native DOM conversion.
Description check ✅ Passed The pull request description clearly relates to the changeset. It references a specific issue, identifies tested areas (honeypot, form validation, reCAPTCHA, buttons, TinyMCE, signatures, etc.), and aligns with the jQuery-to-native-DOM refactoring documented in the summary.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch try_to_use_less_jquery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
js/formidable.js (1)

849-853: AJAX form handling now assumes a DOM form; behavior is consistent but input type is stricter

getFormErrors now:

  • Uses object.querySelectorAll('.frm_form_field') to add/remove frm_doing_ajax.
  • Reads the form id and page order via querySelector('input[name="..."]') on the form or document.
  • Locates field containers, captchas, and buttons via querySelector/All and manipulates them directly.

This is internally consistent assuming object is always an HTMLFormElement (as now required by postToAjaxUrl). If any external callers still pass a jQuery object into frmFrontForm.checkFormErrors, they will now get runtime errors because querySelectorAll and classList are not available.

Given this function is exposed as checkFormErrors, consider either:

  • Documenting that it now requires a plain form element, or
  • Normalizing the input as done elsewhere:
 	function getFormErrors( object, action ) {
-		let data, success, error, shouldTriggerEvent;
+		let data, success, error, shouldTriggerEvent;
+
+		if ( object instanceof jQuery ) {
+			object = object.get( 0 );
+		}
+		if ( ! object ) {
+			return;
+		}
@@
-		const fieldsets = object.querySelectorAll( '.frm_form_field' );
+		const fieldsets = object.querySelectorAll( '.frm_form_field' );

This keeps the new DOM-based logic while gracefully handling existing jQuery call sites.

Also applies to: 923-925, 939-945, 963-965, 986-999, 1009-1010, 1028-1030

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2001d51 and f1ef92a.

📒 Files selected for processing (1)
  • js/formidable.js (25 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Cypress
  • GitHub Check: PHP 7.4 tests in WP trunk
  • GitHub Check: PHP 8 tests in WP trunk
  • GitHub Check: PHP 7.4 tests in WP trunk
  • GitHub Check: Cypress
  • GitHub Check: PHP 8 tests in WP trunk
🔇 Additional comments (19)
js/formidable.js (19)

126-134: disableSubmitButton now robustly handles both jQuery and plain form elements

Normalizing $form to a DOM node and using querySelectorAll().forEach() with button.disabled = true is correct and avoids jQuery .find() while preserving behavior. Early return on falsy form also safely covers empty jQuery selections.


143-150: enableSubmitButton native conversion looks correct

Same pattern as disableSubmitButton: converting $form to a DOM element and toggling disabled = false on the matching controls is behaviorally equivalent to the old jQuery-based implementation.


160-167: disableSaveDraft jQuery → DOM conversion is safe

Converting $form to a DOM element and using querySelectorAll('a.frm_save_draft') with pointerEvents = 'none' correctly mirrors the old .find().css() behavior, with null-guard in place.


458-462: Signature “typed vs output” handling matches intent

Using closest('.frm_form_field') and querySelector('[name="... [output]"]') to derive the output field when the typed value is empty is sound, with null checks so a missing container gracefully falls back to treating the field as empty.


738-744: executeInvisibleRecaptcha correctly expects a DOM element now

Using invisibleRecaptcha.dataset.rid and passing that into grecaptcha.reset/execute aligns with hasInvisibleRecaptcha returning the widget element. Once the .length checks are fixed, this should behave as before without relying on jQuery.


747-775: validateRecaptcha: DOM-based lookup and error wiring look correct

Normalizing form to formEl, early-returning on falsy or missing .frm-g-recaptcha, and then:

  • Reading data-rid via dataset.
  • Using a try/catch around grecaptcha.getResponse.
  • Resolving the field container via closest('.frm_form_field') and deriving its ID.

all mirror the original intent without .find(). Null checks around formEl and the container prevent obvious crashes.


1062-1066: afterFormSubmitted: safer content inspection via a detached div

Parsing response.content into a temporary div and using querySelector('.frm_message') to decide between frmFormComplete and frmPageChanged avoids jQuery .find() and prevents accidental execution of inline scripts during the check. The event payload remains unchanged.


1082-1085: afterFormSubmittedBeforeReplace: consistent with afterFormSubmitted

Same pattern as afterFormSubmitted, but for the “before replace” hook. Using a detached div and querySelector('.frm_message') is appropriate and keeps behavior focused on markup structure rather than jQuery.


1147-1199: addFieldError: DOM-based implementation preserves behavior and improves ARIA handling

Highlights:

  • Accepts either jQuery or HTMLElement, normalizing to a container element.
  • Bails out if the container is hidden (offsetParent === null), matching prior intent.
  • Builds error markup when needed, preserving frm_js.include_alert_role and getErrorElementId.
  • Updates aria-describedby in an order-aware way using data-error-first.
  • Marks either the input or its radiogroup/checkbox group as aria-invalid="true".
  • Still fires the frmAddFieldError jQuery event with a jQuery-wrapped container.

This looks like a faithful port off .find() with better accessibility support.


1220-1260: removeFieldError correctly mirrors addFieldError and handles both DOM and jQuery containers

Key points:

  • Normalizes fieldCont to a DOM element and early-returns if missing.
  • Removes frm_blank_field/has-error classes.
  • Resets aria-invalid either on the input or, for radios/checkboxes, on the associated [role="group"/"radiogroup"].
  • Removes the first .frm_error element, then carefully cleans aria-describedby, stripping the error ID while keeping any remaining references.

The small detail of temporarily removing aria-describedby and re-adding a cleaned value is fine and keeps attributes in sync.


1263-1268: removeAllErrors: DOM-native cleanup is straightforward and safe

Using three document.querySelectorAll calls to:

  • Clear frm_blank_field / has-error classes from .form-field.
  • Remove .frm_error elements.
  • Remove .frm_error_style elements.

matches the old jQuery behavior and has clear, predictable effects.


1328-1339: showFileLoading: DOM version preserves intent

Switching to document.getElementById('frm_loading') plus object.querySelector('input[type=file]') and checking fileInput.value before showing the loader after 2 seconds retains the original semantics, assuming a single file field per form (which matches how jQuery .val() behaves on a collection).


1347-1349: confirmClick: using dataset.frmconfirm is a clean replacement

Reading the confirmation message via this.dataset.frmconfirm and then confirm(message) is equivalent to using data('frmconfirm') or getAttribute('data-frmconfirm'), and keeps the handler simple and dependency-free.


1358-1361: onHoneypotFieldChange: direct getComputedStyle is appropriate

Using window.getComputedStyle(this).boxShadow and checking for /inset/ is an accurate way to detect WebKit autofill and remove the honeypot field without involving jQuery. The validthis hint is preserved.


1926-1931: submitFormNow: file field detection via DOM APIs looks correct

querySelectorAll('input[type="file"]') combined with Array.from(...).filter(input => !!input.value).length is an accurate port of the old “any file inputs with a value?” logic, and the guarded read of the frm_action hidden input via querySelector is safe.


1946-1948: validateFormSubmit: tinyMCE save guard correctly handles jQuery or DOM forms

Normalizing object to form with instanceof jQuery ? .get(0) : object and checking for .wp-editor-wrap before calling tinyMCE.triggerSave() keeps the integration working whether the caller passes a jQuery-wrapped form or a plain element.


1970-1976: getAjaxFormErrors: action detection via DOM is a reasonable enhancement

The added form normalization and querySelector('input[name="frm_action"]') to derive the action for frmThemeOverride_jsErrors is sound, with null checks so a missing input just yields an empty string. Passing the original object into the override keeps the existing hook contract intact.


1997-2006: addAjaxFormErrors: DOM-based lookup of field containers is solid

Normalizing object to form and using form.querySelector('#frm_field_' + key + '_container') to find each field container, then:

  • Calling addFieldError when found, or
  • Deleting the error key when not,

is a clean port from .find() and keeps error state in sync with the rendered form.


2035-2037: scrollMsg: robust handling of jQuery or DOM object when scrolling to a field

In the typeof id === 'string' branch, resolving formEl via object instanceof jQuery ? object.get(0) : object, then using querySelector('#frm_field_' + id + '_container') and wrapping the result back in jQuery is a good compromise: DOM APIs for lookup, jQuery for existing scroll/offset logic.

Comment thread js/formidable.js
@Crabcyborg Crabcyborg changed the title Stop using jQuery .find() Stop using jQuery .find() Dec 3, 2025
@Crabcyborg Crabcyborg changed the title Stop using jQuery .find() Stop using jQuery .find() in formidable.js Dec 3, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
js/formidable.js (1)

716-735: Invisible reCAPTCHA branch is broken by .length checks on an HTMLElement

hasInvisibleRecaptcha now returns either an HTMLElement or false, but submitFormManual still treats the result as an array-like and checks .length:

  • Line 1873: if ( classList && invisibleRecaptcha.length < 1 ) { ... }
  • Line 1895: if ( invisibleRecaptcha.length ) { ... }

For an HTMLElement, invisibleRecaptcha.length is undefined, so:

  • invisibleRecaptcha.length < 1 is always false, so the Pro-only guard no longer runs when there is no invisible reCAPTCHA.
  • if ( invisibleRecaptcha.length ) never passes, so executeInvisibleRecaptcha is never called even when an invisible widget is present.

This effectively disables the invisible reCAPTCHA flow and changes gating behavior for non‑Pro forms. The checks should use truthiness of the element instead of .length.

Suggested fix:

-			if ( classList && invisibleRecaptcha.length < 1 ) {
+			// If there is no invisible reCAPTCHA, enforce Pro-only restriction.
+			if ( classList && ! invisibleRecaptcha ) {
 				isPro = classList.indexOf( 'frm_pro_form' ) > -1;
 				if ( ! isPro ) {
 					return;
 				}
 			}
@@
-			if ( invisibleRecaptcha.length ) {
+			if ( invisibleRecaptcha ) {
 				showLoadingIndicator( jQuery( object ) );
 				executeInvisibleRecaptcha( invisibleRecaptcha );
 			} else {

This aligns submitFormManual with the new hasInvisibleRecaptcha contract and restores the intended behavior.

Also applies to: 738-744, 746-778, 1868-1903

🧹 Nitpick comments (2)
js/formidable.js (2)

1220-1268: removeFieldError/removeAllErrors: consider removing all .frm_error nodes in a container

The DOM-based removeFieldError and removeAllErrors implementations correctly:

  • Accept either jQuery or HTMLElement.
  • Clear frm_blank_field/has-error classes.
  • Reset aria-invalid on inputs or their radio/checkbox groups.
  • Clean up aria-describedby to drop the specific error ID.

One small robustness improvement: removeFieldError currently removes only the first .frm_error in the container:

const errorMessage = container.querySelector( '.frm_error' );
const errorId = errorMessage ? errorMessage.id : '';
...
if ( errorMessage ) {
	errorMessage.remove();
}

If a theme override or custom code ever injects multiple .frm_error elements into a single field container, the extras would be left behind. You can cheaply handle that case while still using the first ID for aria-describedby cleanup:

-		const errorMessage = container.querySelector( '.frm_error' );
-		const errorId = errorMessage ? errorMessage.id : '';
+		const errorMessages = container.querySelectorAll( '.frm_error' );
+		const errorId = errorMessages.length ? errorMessages[ 0 ].id : '';
@@
-		if ( errorMessage ) {
-			errorMessage.remove();
-		}
+		if ( errorMessages.length ) {
+			errorMessages.forEach( msg => msg.remove() );
+		}

removeAllErrors already clears all .frm_error and .frm_error_style elements globally, so this is just a defensive improvement for the per-field path.


848-1035: Normalize DOM/jQuery handling in form-level helpers to reduce duplication and hardening risk

Across getFormErrors, submitFormNow, validateFormSubmit, getAjaxFormErrors, addAjaxFormErrors, and scrollMsg, you’re sometimes treating object as a DOM node (using querySelector(All), classList, etc.) and in other places wrapping it in jQuery (jQuery( object )) or normalizing to a form variable (object instanceof jQuery ? object.get( 0 ) : object).

Internally this works because current call sites pass an actual <form> element into these methods, but frmFrontForm.checkFormErrors and friends are part of the public API surface and could be called with a jQuery form in custom code.

To make this more robust and DRY:

  • In each of these helpers, derive a single formEl up front, e.g.:

    const formEl = object instanceof jQuery ? object.get( 0 ) : object;
    if ( ! formEl ) {
    	return;
    }
  • Use formEl consistently for DOM APIs (querySelectorAll, classList.contains, closest, etc.) and only wrap it in jQuery when you explicitly need jQuery behavior (serialization, events).

This would:

  • Guard against a potential TypeError if any external caller passes a jQuery form into frmFrontForm.checkFormErrors.
  • Remove repeated instanceof jQuery ? .get(0) snippets spread across multiple methods.

Also applies to: 1925-1937, 1945-1990, 1996-2014, 2021-2069

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1ef92a and d378bc5.

📒 Files selected for processing (1)
  • js/formidable.js (25 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: PHP 8 tests in WP trunk
  • GitHub Check: Cypress
  • GitHub Check: PHP 7.4 tests in WP trunk
🔇 Additional comments (5)
js/formidable.js (5)

126-168: Submit / Save Draft controls: DOM refactor looks sound

The conversion from $form.find(...) to resolving a real form element ($form instanceof jQuery ? $form.get( 0 ) : $form) plus querySelectorAll(...).forEach(...) to toggle disabled/pointerEvents is correct and safely no-ops when given an empty jQuery object or falsy input. Behavior for submit buttons and Save Draft links should remain unchanged.


193-247: validateForm: safer DOM access while preserving behavior

Normalizing object to a plain element (vanillaJsObject) and using querySelectorAll with optional chaining prevents errors when a jQuery object or falsy value is passed in, while keeping the existing required-field and recaptcha validation semantics intact. The invisible vs visible reCAPTCHA split via hasInvisibleRecaptcha( object ) continues to behave correctly.


387-482: Required field handling for typed signature fields is improved

The new branch that, for signature fields, falls back to the [output] field inside the same .frm_form_field when the [typed] input is empty is a solid enhancement. The closest/querySelector usage is null-safe, and the subsequent fieldID normalization (replace('-typed', '')) keeps IDs consistent with previous behavior.


1147-1199: addFieldError: HTMLElement/jQuery interop and ARIA updates preserved

Handling either a jQuery object or an HTMLElement in addFieldError via instanceof jQuery ? .get(0) : ... is clean, and the early return when container.offsetParent === null correctly skips hidden fields. The new DOM-based implementation still:

  • Adds frm_blank_field to the container.
  • Appends the error HTML (or defers to frmThemeOverride_frmPlaceError).
  • Maintains aria-describedby ordering based on data-error-first.
  • Marks the input or its radiogroup/checkbox group as aria-invalid="true".

The final jQuery event trigger (frmAddFieldError) maintains backward compatibility with existing listeners.


1328-1363: showFileLoading / confirmClick / honeypot handler: lean DOM usage looks correct

The refactors here look good:

  • showFileLoading now uses document.getElementById/querySelector with simple null checks, preserving the “only show loading when a file has a value for 2s” behavior.
  • confirmClick reading this.dataset.frmconfirm is equivalent to the old data('frmconfirm') for data-frmconfirm attributes.
  • onHoneypotFieldChange using getComputedStyle( this ).boxShadow and testing /inset/ keeps the previous autofill-detection semantics without needing jQuery.

All three are safe, side-effect free refactors away from .find() and jQuery-only APIs.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (6)
js/formidable.js (6)

126-133: Submit/save‑draft helpers: DOM conversion looks good; consider normalizing argument types

The conversions to querySelectorAll with HTMLElement|jQuery handling are sane and avoid .find(). One minor nit is that enableSaveDraft still assumes a jQuery object while disableSaveDraft/disableSubmitButton/enableSubmitButton now normalize both. For long‑term consistency, you might consider normalizing the $form argument in enableSaveDraft the same way so all four helpers accept either type.

Also applies to: 143-150, 161-167


712-737: Invisible reCAPTCHA flow now treats the element correctly; consider guarding grecaptcha access

hasInvisibleRecaptcha returning an HTMLElement|false and submitFormManual checking if ( invisibleRecaptcha ) / ! invisibleRecaptcha fixes the earlier .length bug and matches the updated contract. executeInvisibleRecaptcha and validateRecaptcha also correctly pull the widget ID from data-rid and work on the DOM element.

One robustness improvement: hasInvisibleRecaptcha and executeInvisibleRecaptcha call grecaptcha without a guard, whereas the reset logic in getFormErrors wraps grecaptcha usage in a typeof grecaptcha !== 'undefined' check. To avoid hard failures on misconfigured pages/tests where .frm-g-recaptcha markup exists but the API isn’t loaded yet, you could mirror that guard (or bail out early if typeof grecaptcha === 'undefined').

Also applies to: 740-748, 751-777


852-857: getFormErrors DOM refactor is sound, but it now assumes a raw HTMLElement

The refactor to use object.querySelectorAll/classList and DOM parsing for response.content looks correct and the invisible/turnstile captcha resets are preserved.

One behavioral change to note: getFormErrors (and thus frmFrontForm.checkFormErrors) now assumes object is an HTMLElement and calls querySelectorAll directly, while most other entry points (validateFormSubmit, getAjaxFormErrors, addAjaxFormErrors, scrollMsg) are tolerant of either HTMLElement or jQuery and normalize with instanceof jQuery. If any third‑party code calls frmFrontForm.checkFormErrors( jQuery(... ) ), this will now throw.

If you want to keep backward compatibility aligned with the rest of the API, you could normalize here as well, e.g.:

-	function getFormErrors( object, action ) {
+	function getFormErrors( object, action ) {
+		const form = object instanceof jQuery ? object.get( 0 ) : object;
+		if ( ! form ) {
+			return;
+		}
-		const fieldsets = object.querySelectorAll( '.frm_form_field' );
+		const fieldsets = form.querySelectorAll( '.frm_form_field' );
...
-		data = jQuery( object ).serialize() + '&action=frm_entries_' + action + '&nonce=' + frm_js.nonce;
-		shouldTriggerEvent = object.classList.contains( 'frm_trigger_event_on_submit' );
+		data = jQuery( form ).serialize() + '&action=frm_entries_' + action + '&nonce=' + frm_js.nonce;
+		shouldTriggerEvent = form.classList.contains( 'frm_trigger_event_on_submit' );
...
-			const formIdInput = object.querySelector( 'input[name="form_id"]' );
+			const formIdInput = form.querySelector( 'input[name="form_id"]' );
...
-					const fieldContEl = object.querySelector( '#frm_field_' + key + '_container' );
+					const fieldContEl = form.querySelector( '#frm_field_' + key + '_container' );
...
-				object.querySelectorAll( '.frm-g-recaptcha, .g-recaptcha, .h-captcha' ).forEach(
+				form.querySelectorAll( '.frm-g-recaptcha, .g-recaptcha, .h-captcha' ).forEach(
...
-					object.querySelectorAll( '.frm-cf-turnstile' ).forEach(
+					form.querySelectorAll( '.frm-cf-turnstile' ).forEach(
...
-			object.querySelectorAll( 'input[type="submit"], input[type="button"]' ).forEach(
+			form.querySelectorAll( 'input[type="submit"], input[type="button"]' ).forEach(

and keep passing the original object where you intentionally want the caller’s type (e.g., when triggering events).

Also applies to: 927-929, 943-949, 966-969, 990-1007, 1013-1013, 1031-1034


1152-1203: addFieldError DOM refactor is good; small ARIA improvement for radios/checkboxes

The new implementation correctly:

  • Normalizes HTMLElement|jQuery containers.
  • Skips hidden containers via offsetParent.
  • Builds error markup or uses theme overrides as before.
  • Maintains aria-describedby ordering with the data-error-first convention.
  • Sets aria-invalid on either the grouped control or the input.

One subtle improvement: when the field is a radio/checkbox but there is no [role="radiogroup"]/[role="group"] wrapper, nothing gets marked invalid. A simple fallback keeps accessibility intact:

-		if ( 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' );
-			}
-		}
+		if ( 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' );
+				}
+			} else {
+				input.setAttribute( 'aria-invalid', 'true' );
+			}
+		}

This preserves the preferred group behavior but still flags standalone radio/checkbox inputs as invalid.


1224-1264: removeFieldError / removeAllErrors: ARIA cleanup could be more complete for AJAX error cycles

removeFieldError mirrors the new addFieldError behavior well: it normalizes HTMLElement|jQuery, removes classes, clears aria-invalid (input or group), drops the error node, and strips its ID from aria-describedby while preserving other IDs.

removeAllErrors, however, only:

  • Removes classes from .form-field wrappers.
  • Removes all .frm_error and .frm_error_style elements.

If a field had errors on a previous AJAX submission but no longer has any, its input can still be left with aria-invalid="true" and an aria-describedby pointing at an ID that no longer exists, because removeFieldError is never called in that path.

If you want the ARIA state to reflect the current error set, consider using removeFieldError per field container, e.g.:

-	function removeAllErrors() {
-		document.querySelectorAll( '.form-field' ).forEach( field => {
-			field.classList.remove( 'frm_blank_field', 'has-error' );
-		} );
-		document.querySelectorAll( '.form-field .frm_error' ).forEach( error => error.remove() );
-		document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
-	}
+	function removeAllErrors() {
+		document.querySelectorAll( '.form-field' ).forEach( field => {
+			removeFieldError( field );
+		} );
+		document.querySelectorAll( '.frm_error_style' ).forEach( error => error.remove() );
+	}

You may want to extend removeFieldError to remove all .frm_error children if multiple can exist per field.

Also applies to: 1267-1272


1950-1965: Form‑level validation path: good normalization; minor consistency nits

The validation/get‑errors/add‑errors/scroll path looks solid:

  • validateFormSubmit now only calls tinyMCE.triggerSave() when the current form actually contains .wp-editor-wrap, using a normalized form element.
  • getAjaxFormErrors and addAjaxFormErrors correctly normalize HTMLElement|jQuery to a DOM element for DOM queries but still pass the original object to theme overrides/events.
  • addAjaxFormErrors now uses querySelector('#frm_field_' + key + '_container') and calls addFieldError with a plain element, which matches the refactored helper.
  • scrollMsg’s new typeof id === 'string' branch correctly handles the (fieldId, form) signature using DOM queries while keeping existing jQuery‑only call patterns working.

The only consistency nit is that some helpers (validateFormSubmit, getAjaxFormErrors, addAjaxFormErrors, scrollMsg) normalize object instanceof jQuery, while getFormErrors currently does not. Aligning getFormErrors with this pattern (as noted in the earlier comment) would make the API surface more predictable.

Also applies to: 1972-1984, 2000-2018, 2039-2042

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d378bc5 and 2ff992b.

📒 Files selected for processing (1)
  • js/formidable.js (26 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cypress
🔇 Additional comments (6)
js/formidable.js (6)

459-462: Typed signature required check: fallback handling looks safe

Using closest('.frm_form_field') and a scoped querySelector to pull the [output] input is a reasonable replacement and safely handles missing containers/outputs by falling back to an empty string.


1066-1069: Using a temporary div to inspect response.content is fine

Parsing response.content via a detached <div> and checking for .frm_message before firing completion/page‑change events is a clean, side‑effect‑free replacement for the older jQuery traversal.

Also applies to: 1086-1089


1332-1343: showFileLoading DOM usage is straightforward and safe

The switch to document.getElementById and object.querySelector('input[type=file]') with a null‑safe value check keeps the old behavior while avoiding .find(). It’s only called with a real form element, so the direct DOM usage is appropriate.


1352-1353: confirmClick and honeypot change handlers: dataset/getComputedStyle usage looks fine

Using this.dataset.frmconfirm and window.getComputedStyle( this ).boxShadow is a clean, modern replacement. Both handlers are only bound to real DOM elements via jQuery delegation, so relying on dataset and getComputedStyle here is reasonable.

Also applies to: 1362-1365


1777-1783: Invisible reCAPTCHA checks in submitFormManual now align with the updated helper

The updated checks:

  • if ( classList && ! invisibleRecaptcha ) { ... } for the Pro gating.
  • if ( invisibleRecaptcha ) { executeInvisibleRecaptcha( invisibleRecaptcha ); } else { ... }

correctly treat hasInvisibleRecaptcha’s return value as an element/false instead of relying on .length, addressing the earlier dead‑code path for invisible reCAPTCHA execution.

Also applies to: 1899-1902


1929-1935: File field detection/frm_action lookup via DOM is correct

Using querySelectorAll('input[type="file"]') plus Array.from(...).filter( input => !! input.value ).length matches the old jQuery semantics and properly gates the AJAX path vs full submit. The null‑safe frm_action lookup via querySelector is also appropriate.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
js/formidable.js (1)

852-1040: getFormErrors uses DOM APIs directly, risking errors if jQuery objects are passed

The exported checkFormErrors alias for getFormErrors directly calls DOM APIs on the object parameter:

  • object.querySelectorAll(...)
  • object.classList.contains(...)
  • object.submit()
  • object.querySelector(...)
  • object.insertAdjacentHTML(...)

If external code calls frmFrontForm.checkFormErrors( jQuery( form ), action ), these DOM API calls will fail on the jQuery instance. The same applies to postToAjaxUrl, which is called with object.

To maintain backward compatibility, normalize the input once:

 function getFormErrors( object, action ) {
-		let data, success, error, shouldTriggerEvent;
-
-		const fieldsets = object.querySelectorAll( '.frm_form_field' );
+		let data, success, error, shouldTriggerEvent;
+
+		const form = object instanceof jQuery ? object.get( 0 ) : object;
+		if ( ! form ) {
+			return;
+		}
+
+		const fieldsets = form.querySelectorAll( '.frm_form_field' );
 		fieldsets.forEach( field => field.classList.add( 'frm_doing_ajax' ) );

Replace remaining object references with form throughout the function.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff992b and ae5ecdc.

📒 Files selected for processing (1)
  • js/formidable.js (26 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: PHP 8 tests in WP trunk
  • GitHub Check: PHP 7.4 tests in WP trunk
  • GitHub Check: Cypress
🔇 Additional comments (10)
js/formidable.js (10)

126-167: Submit/save button & draft link handling with native DOM APIs looks solid

Normalizing $form to a single HTMLElement and using querySelectorAll to toggle disabled and pointerEvents keeps behavior consistent for both jQuery and raw DOM callers. This matches existing call sites (which pass a single form) and keeps the save‑draft behavior separated but coherent.


387-470: Signature “typed” value resolution looks correct

The new logic that, for empty typed signature inputs, finds the [output] field within the same .frm_form_field container and uses its value before stripping -typed from fieldID matches the expected signature field structure and maintains prior semantics while avoiding jQuery .find(). No issues spotted.


750-782: validateRecaptcha HTMLElement/jQuery support and error mapping look good

The refactor to normalize form/formEl, query .frm-g-recaptcha, use dataset.rid, and map an empty response to an error key derived from .frm_form_field’s ID keeps behavior aligned with the previous jQuery version while handling both jQuery and raw DOM inputs safely. The try/catch around grecaptcha.getResponse with the recaptcha_checked escape hatch is also preserved correctly.


1066-1093: DOM‑fragment based detection of completion message is a nice cleanup

Using a temporary div and querySelector( '.frm_message' ) in both afterFormSubmitted and afterFormSubmittedBeforeReplace is a straightforward and robust way to distinguish between “form complete” and “page changed” responses. That avoids brittle string checks on response.content and doesn’t change the contract of the triggered events.


1152-1273: Error rendering/clearing refactor improves HTMLElement/jQuery interop and ARIA handling

The updates to addFieldError, removeFieldError, and removeAllErrors look good:

  • They gracefully accept either a jQuery object or an HTMLElement.
  • They avoid .find() in favor of querySelector and closest.
  • ARIA attributes (aria-describedby, aria-invalid) are applied and cleared carefully, including grouping for radio/checkbox fields via [role="radiogroup"], [role="group"].
  • Hidden/non‑rendered containers short‑circuit via offsetParent === null, which prevents errors being attached to collapsed/hidden sections.

The logic for preserving any existing aria-describedby tokens while removing only the error element ID is also correct. No functional issues spotted here.


1332-1368: File‑upload spinner and honeypot/autofill detection refactor look correct

  • showFileLoading now uses document.getElementById and object.querySelector( 'input[type=file]' ), mirroring the old .find()/.val() behavior while still using jQuery only for the fadeIn animation. It safely exits when the loading element or file input is missing.
  • confirmClick and onHoneypotFieldChange correctly switch to dataset.frmconfirm and getComputedStyle( this ).boxShadow respectively, using this from delegated event handlers as before.

These changes avoid jQuery .find() without altering observable behavior.


1755-1852: Captcha label fix and render path are consistent and robust

The new maybeFixCaptchaLabel helper and its use in renderCaptcha:

  • Correctly locate the parent <form> of the captcha element.
  • Find both the reCAPTCHA and Turnstile labels and response inputs and ensure label.htmlFor matches the actual response input id.
  • Are called after data-rid is set, so DOM is ready.

This should resolve label/response mismatches without impacting existing render behavior for visible or invisible captchas.


1873-1935: Invisible reCAPTCHA execution path now correctly uses element truthiness

submitFormManual now treats the value from hasInvisibleRecaptcha as an HTMLElement|false and uses a simple truthy check:

if ( invisibleRecaptcha ) {
    showLoadingIndicator( jQuery( object ) );
    executeInvisibleRecaptcha( invisibleRecaptcha );
} else {
    showSubmitLoading( jQuery( object ) );
    frmFrontForm.submitFormNow( object );
}

This fixes the earlier .length misuse while preserving the intended flow of:

  • Execute invisible reCAPTCHA when present and not yet checked.
  • Otherwise show the normal submit loading state and submit (AJAX or normal).

The additional checks in submitFormNow for file fields and antispam/unique ID inputs remain intact.


1943-2012: Normalization of form elements in validation and AJAX error rendering looks good

The refactors in:

  • validateFormSubmit (using form.querySelector( '.wp-editor-wrap' ) before tinyMCE.triggerSave()),
  • getAjaxFormErrors (using a normalized form to read frm_action),
  • addAjaxFormErrors (normalizing object to form and querying containers with querySelector),

all correctly support both jQuery and plain HTMLElement inputs while removing .find() usage. The use of scrollToFirstField( object ) and checkForErrorsAndMaybeSetFocus() after populating jsErrors maintains the existing UX. Aside from the separate getFormErrors concern noted earlier, these pieces look solid.


2024-2067: scrollMsg DOM lookup for field containers maintains behavior

In the typeof id === 'string' branch, converting a possible jQuery object to a raw form element and then using querySelector( '#frm_field_' + id + '_container' ) before wrapping it back in jQuery keeps the old semantics while avoiding .find(). This still supports both jQuery and raw form objects without changing how scrollObj.offset().top is computed downstream.

Comment thread js/formidable.js
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
js/formidable.js (2)

744-748: Missing grecaptcha guards in executeInvisibleRecaptcha.

Lines 746-747 call grecaptcha.reset() and grecaptcha.execute() without verifying that grecaptcha is defined. This will throw a ReferenceError if the reCAPTCHA script is unavailable.

Apply this diff to guard all grecaptcha calls:

 	function executeInvisibleRecaptcha( invisibleRecaptcha ) {
+		if ( typeof grecaptcha === 'undefined' || ! grecaptcha ) {
+			return;
+		}
 		const recaptchaID = invisibleRecaptcha.dataset.rid;
 		grecaptcha.reset( recaptchaID );
 		grecaptcha.execute( recaptchaID );
 	}

717-737: Missing grecaptcha guard causes ReferenceError when reCAPTCHA script is blocked.

Line 730 calls grecaptcha.getResponse(recaptchaID) without checking if grecaptcha is defined. If the Google reCAPTCHA script fails to load or is blocked by an ad blocker, this throws a ReferenceError and breaks form submission.

Line 993 demonstrates the correct guard pattern used elsewhere: typeof grecaptcha !== 'undefined' && grecaptcha.

Apply this diff to guard grecaptcha access:

 		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 );
+			let alreadyChecked = '';
+			if ( typeof grecaptcha !== 'undefined' && grecaptcha ) {
+				alreadyChecked = grecaptcha.getResponse( recaptchaID );
+			}
 			if ( alreadyChecked.length === 0 ) {
 				return recaptcha;
 			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae5ecdc and 0724f62.

📒 Files selected for processing (1)
  • js/formidable.js (26 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Cypress
  • GitHub Check: PHP 7.4 tests in WP trunk
  • GitHub Check: PHP 8 tests in WP trunk
🔇 Additional comments (3)
js/formidable.js (3)

990-1004: No action needed. Both hcaptcha (line 1001) and turnstile (line 1006) already have proper safety guards in the reset logic. The hcaptcha check uses typeof hcaptcha !== 'undefined' && hcaptcha and turnstile uses if ( window.turnstile ), consistent with the grecaptcha handling pattern.


127-150: The asymmetry between disableSubmitButton and enableSubmitButton regarding button.frm_save_draft is intentional and not an issue. Analysis of call sites shows these functions are always invoked as paired operations: disableSubmitButton and disableSaveDraft are called together (line 1292–1293), and enableSubmitButton and enableSaveDraft are called together (line 1327–1328). This design pattern ensures all disabled elements are properly re-enabled through separate, responsibility-specific functions. No changes needed.


1889-1893: All call sites correctly handle the HTMLElement | false return type.

Verification confirms both call sites handle the return type appropriately:

  • Line 242: if ( ! hasInvisibleRecaptcha( object ) ) — negation operator correctly handles both false and HTMLElement
  • Line 1889: Assigns result to const invisibleRecaptcha, then uses truthy check if ( invisibleRecaptcha ) — correctly distinguishes false from HTMLElement

Comment thread js/formidable.js
Comment thread js/formidable.js
@Crabcyborg Crabcyborg merged commit fdbce3e into master Dec 4, 2025
16 checks passed
@Crabcyborg Crabcyborg deleted the try_to_use_less_jquery branch December 4, 2025 14:40
stephywells pushed a commit that referenced this pull request Apr 4, 2026
Stop using jQuery `.find()` in `formidable.js`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant