Skip to content

Commit 406cf3e

Browse files
committed
Fix wet-boew/JQV validation of checkbox groups, allow any checkbox to suffice required marker and prevent duplicate error markers for the group
1 parent 2c95f23 commit 406cf3e

File tree

3 files changed

+193
-2
lines changed

3 files changed

+193
-2
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* Collapse WET-BOEW / jQuery Validate errors for Webform checkbox *groups* so:
3+
* - "at least one checked" is enforced via require_from_group
4+
* - only ONE inline error is rendered for the whole set
5+
* - only ONE item appears in the WET error summary
6+
* - checking ANY checkbox in the set clears the group error
7+
*
8+
* How it works (high level):
9+
* 1) We wait until WET's validator is initialized on the form.
10+
* 2) We discover checkbox groups by looking for inputs named like base[option]
11+
* that also have data-rule-require_from_group (added in PHP preprocess).
12+
* 3) We register jQuery Validate "groups" for each base so the set acts as one field.
13+
* 4) We only *place* an inline error label for the FIRST checkbox of each base.
14+
* (The others are skipped to avoid duplicate labels.)
15+
* 5) We de-duplicate the validator's error list in invalidHandler so WET's top
16+
* summary only contains one entry per base.
17+
* 6) We prevent submission when the validator reports invalid, covering
18+
* normal submit + Enter key + direct submit-button clicks.
19+
*/
20+
21+
(function ($, Drupal, once) {
22+
Drupal.behaviors.wetGroupOneMessage = {
23+
attach(context) {
24+
// Bind this behavior once per element with .wb-frmvld.
25+
const hosts = once('wetGroupOneMessage', '.wb-frmvld', context);
26+
27+
hosts.forEach((host) => {
28+
const $host = $(host);
29+
30+
// Find the <form> that is being validated.
31+
// In WxT it’s usually a child of .wb-frmvld.
32+
const $form = $host.is('form') ? $host : $host.find('form').first();
33+
if (!$form.length) return;
34+
35+
/**
36+
* Given a name like "select_from_the_following[2]" return the base "select_from_the_following".
37+
* Returns null for non-array names.
38+
*/
39+
const baseOf = (name) => (name || '').match(/^([^\[]+)\[.+\]$/)?.[1] || null;
40+
41+
/**
42+
* Discover checkbox groups on the form.
43+
* We only consider checkboxes that:
44+
* - have a name like base[option]
45+
* - and have data-rule-require_from_group (added server-side)
46+
*
47+
* Returns a map: { baseName: [ "base[1]", "base[2]", ... ] }
48+
*/
49+
function discoverGroups() {
50+
const map = {};
51+
$form.find('input[type=checkbox][name*="["][data-rule-require_from_group]').each(function () {
52+
const base = baseOf(this.name);
53+
if (!base) return;
54+
(map[base] ||= []).push(this.name);
55+
});
56+
return map;
57+
}
58+
59+
/**
60+
* Wire our grouping + dedup logic into the existing validator instance.
61+
* @param {Object} v - jQuery Validate instance stored on the form by WET.
62+
*/
63+
function wire(v) {
64+
const groups = discoverGroups();
65+
66+
// 1) Register one jQuery Validate "group" per checkbox base.
67+
// This makes jQuery Validate treat multiple field names as one logical group
68+
// when deciding which single label/entry to render.
69+
v.settings.groups = v.settings.groups || {};
70+
Object.keys(groups).forEach((base) => {
71+
// Names must be space-separated for JQV "groups".
72+
v.settings.groups[base] = groups[base].join(' ');
73+
});
74+
75+
// Precompute the "first" field name for each base; duplicates will be suppressed.
76+
const firstOf = {};
77+
Object.keys(groups).forEach((base) => {
78+
firstOf[base] = groups[base][0];
79+
});
80+
81+
// Shadow a local baseOf so inner functions have it in scope (performance/readability).
82+
const baseOf = (name) => (name || '').match(/^([^\[]+)\[.+\]$/)?.[1] || null;
83+
84+
// 2) Override errorPlacement to only place the inline label for the FIRST checkbox.
85+
// This avoids multiple inline labels stacked under the group.
86+
const origPlace = v.settings.errorPlacement || function (error, element) { error.insertAfter(element); };
87+
v.settings.errorPlacement = function (error, element) {
88+
const name = element.attr('name');
89+
const base = baseOf(name);
90+
91+
// If this is part of a checkbox base and it's NOT the first item,
92+
// skip placing the label entirely (the "first" item will get it).
93+
if (base && name !== firstOf[base]) {
94+
return;
95+
}
96+
return origPlace(error, element);
97+
};
98+
99+
// 3) Override invalidHandler to de-duplicate BEFORE WET composes its summary.
100+
// We reduce validator.errorList/errorMap to contain only the first failing
101+
// element of each base, so the top summary has one entry per group.
102+
const origInvalid = v.settings.invalidHandler || $.noop;
103+
v.settings.invalidHandler = function (formEl, validator) {
104+
const seen = new Set();
105+
const list = [];
106+
const map = {};
107+
validator.errorList.forEach((item) => {
108+
const nm = item.element && item.element.name;
109+
// Fall back to name for non-array fields.
110+
const base = baseOf(nm) || nm;
111+
112+
// Keep the first error per base, drop the rest.
113+
if (!seen.has(base)) {
114+
seen.add(base);
115+
list.push(item);
116+
if (nm) map[nm] = item.message;
117+
}
118+
});
119+
120+
// Replace the validator's error structures with our reduced versions.
121+
validator.errorList = list;
122+
validator.errorMap = map;
123+
124+
// Allow any original invalidHandler (including WET's) to run with the reduced list.
125+
return origInvalid.call(this, formEl, validator);
126+
};
127+
}
128+
129+
/**
130+
* Try to find the jQuery Validate instance created by WET for this form.
131+
* If found, wire our grouping/dedup logic into it.
132+
* Returns true if the validator exists and was wired.
133+
*/
134+
function tryWire() {
135+
const v = $form.data('validator') || $host.data('validator');
136+
if (v) wire(v);
137+
return !!v;
138+
}
139+
140+
// If WET's validator is already present, wire immediately.
141+
if (tryWire()) return;
142+
143+
// Otherwise, wait for WET to finish attaching the validator, then wire.
144+
// 'wb-ready.wb-frmvld' fires when the plugin is ready on this wrapper.
145+
$host.on('wb-ready.wb-frmvld', tryWire);
146+
147+
// ---- Submission guards --------------------------------------------------
148+
// Prevent the form from submitting when invalid (covers:
149+
// - clicking the submit button
150+
// - pressing Enter in a field
151+
// - any custom handler that falls back to form.submit())
152+
//
153+
// We *evaluate* validity using $form.valid()/v.checkForm() and block the
154+
// event if invalid. We also focus the first invalid field for accessibility.
155+
156+
// Guard the native form submit event.
157+
$form.on('submit.wetGroupOneMessage', function (e) {
158+
const v = $form.data('validator') || $host.data('validator');
159+
// If no validator, let normal processing happen.
160+
if (!v) return;
161+
162+
// valid() calls checkForm() and sets up error display without submitting.
163+
const ok = $form.valid ? $form.valid() : v.checkForm();
164+
if (!ok) {
165+
v.focusInvalid && v.focusInvalid();
166+
e.preventDefault();
167+
e.stopImmediatePropagation();
168+
e.stopPropagation();
169+
return false;
170+
}
171+
});
172+
173+
// Guard submit button clicks as well to catch flows
174+
// where other handlers might try to submit programmatically.
175+
$form.find('button[type=submit], input[type=submit]').on('click.wetGroupOneMessage', function (e) {
176+
const v = $form.data('validator') || $host.data('validator');
177+
if (!v) return;
178+
const ok = $form.valid ? $form.valid() : v.checkForm();
179+
if (!ok) {
180+
v.focusInvalid && v.focusInvalid();
181+
e.preventDefault();
182+
e.stopImmediatePropagation();
183+
e.stopPropagation();
184+
return false;
185+
}
186+
});
187+
});
188+
}
189+
};
190+
})(jQuery, Drupal, once);

modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.libraries.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
webform_required_marker:
1+
wet_webform_enhancements:
22
js:
33
js/webform_required_marker.js: {}
4+
js/webform_checkboxes_group.js: {}
45
dependencies:
56
- core/drupal
67
- core/once

modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.module

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ function wxt_ext_webform_webform_submission_form_alter(array &$form, FormStateIn
168168
$form['#suffix'] = '</div>' . ($form['#suffix'] ?? '');
169169

170170
// Attach label/required enhancer only when inline validation is enabled.
171-
$form['#attached']['library'][] = 'wxt_ext_webform/webform_required_marker';
171+
$form['#attached']['library'][] = 'wxt_ext_webform/wet_webform_enhancements';
172172
}
173173
}
174174

0 commit comments

Comments
 (0)