diff --git a/modules/custom/wxt_ext/wxt_ext_webform/js/webform_checkboxes_group.js b/modules/custom/wxt_ext/wxt_ext_webform/js/webform_checkboxes_group.js new file mode 100644 index 00000000..248e7a49 --- /dev/null +++ b/modules/custom/wxt_ext/wxt_ext_webform/js/webform_checkboxes_group.js @@ -0,0 +1,209 @@ +/** + * Collapse WET-BOEW / jQuery Validate errors for Webform checkbox *groups* so: + * - "at least one checked" is enforced via require_from_group + * - only ONE inline error is rendered for the whole set + * - only ONE item appears in the WET error summary + * - checking ANY checkbox in the set clears the group error + * + * How it works (high level): + * 1) We wait until WET's validator is initialized on the form. + * 2) We discover checkbox groups by looking for inputs named like base[option] + * that also have data-rule-require_from_group (added in PHP preprocess). + * 3) We register jQuery Validate "groups" for each base so the set acts as one field. + * 4) We only *place* an inline error label for the FIRST checkbox of each base. + * (The others are skipped to avoid duplicate labels.) + * 5) We de-duplicate the validator's error list in invalidHandler so WET's top + * summary only contains one entry per base. + * 6) We prevent submission when the validator reports invalid, covering + * normal submit + Enter key + direct submit-button clicks. + */ + +(function ($, Drupal, once) { + Drupal.behaviors.wetGroupOneMessage = { + attach(context) { + // Bind this behavior once per element with .wb-frmvld. + const hosts = once('wetGroupOneMessage', '.wb-frmvld', context); + + hosts.forEach((host) => { + const $host = $(host); + + // Find the
that is being validated. + // In WxT it’s usually a child of .wb-frmvld. + const $form = $host.is('form') ? $host : $host.find('form').first(); + if (!$form.length) return; + + /** + * Given a name like "select_from_the_following[2]" return the base "select_from_the_following". + * Returns null for non-array names. + */ + const baseOf = (name) => (name || '').match(/^([^\[]+)\[.+\]$/)?.[1] || null; + + /** + * Discover checkbox groups on the form. + * We only consider checkboxes that: + * - have a name like base[option] + * - and have data-rule-require_from_group (added server-side) + * + * Returns a map: { baseName: [ "base[1]", "base[2]", ... ] } + */ + function discoverGroups() { + const map = {}; + $form.find('input[type=checkbox][name*="["][data-rule-require_from_group]').each(function () { + const base = baseOf(this.name); + if (!base) return; + (map[base] ||= []).push(this.name); + }); + return map; + } + + /** + * Wire our grouping + dedup logic into the existing validator instance. + * @param {Object} v - jQuery Validate instance stored on the form by WET. + */ + function wire(v) { + const groups = discoverGroups(); + + // Make jQuery Validate prefer element.id over name (fixes accented name issues) + v.idOrName = function (el) { + return el.id || (el.name ? el.name.replace(/[^\w\-]+/g, '_') : ''); + }; + + // 1) Register one jQuery Validate "group" per checkbox base. + // This makes jQuery Validate treat multiple field names as one logical group + // when deciding which single label/entry to render. + v.settings.groups = v.settings.groups || {}; + Object.keys(groups).forEach((base) => { + // Names must be space-separated for JQV "groups". + v.settings.groups[base] = groups[base].join(' '); + }); + + // Precompute the FIRST field *id* of each base so we can place a single inline label. + const firstIdOf = {}; + Object.keys(groups).forEach((base) => { + const firstName = groups[base][0]; + const $el = $form.find('[name="' + CSS.escape(firstName) + '"]').first(); + firstIdOf[base] = $el.attr('id') || ''; + }); + + // Shadow a local baseOf so inner functions have it in scope (performance/readability). + const baseOf = (name) => (name || '').match(/^([^\[]+)\[.+\]$/)?.[1] || null; + + // 2) Override errorPlacement to only place the inline label for the FIRST checkbox. + // This avoids multiple inline labels stacked under the group. + const origPlace = v.settings.errorPlacement || function (error, element) { error.insertAfter(element); }; + v.settings.errorPlacement = function (error, element) { + const name = element.attr('name'); + const base = baseOf(name); + + // If this is part of a checkbox base and it's NOT the first item, + // skip placing the label entirely (the "first" item will get it). + if (base) { + const id = element.attr('id') || ''; + // Only place for the FIRST id in the group, skip others to avoid duplicates. + if (firstIdOf[base] && id !== firstIdOf[base]) return; + } + return origPlace(error, element); + }; + + // 3) Override invalidHandler to de-duplicate BEFORE WET composes its summary. + // We reduce validator.errorList/errorMap to contain only the first failing + // element of each base, so the top summary has one entry per group. + const origInvalid = v.settings.invalidHandler || $.noop; + v.settings.invalidHandler = function (formEl, validator) { + const seen = new Set(); + const list = []; + const map = {}; + + validator.errorList.forEach((item) => { + const el = item.element; + const nm = el && el.name; + const id = el && el.id; + const base = baseOf(nm) || nm; + const dedupeKey = base + '::' + (id || ''); + + if (!seen.has(dedupeKey)) { + seen.add(dedupeKey); + list.push(item); + if (nm) map[nm] = item.message; + } + }); + + // Replace the validator's error structures with our reduced versions. + validator.errorList = list; + validator.errorMap = map; + + // Clean up any duplicate labels that may exist (caused earlier by accented names) + const seenLabelFor = {}; + $(formEl).find('label.error[for]').each(function () { + const f = $(this).attr('for'); + if (seenLabelFor[f]) $(this).remove(); + else seenLabelFor[f] = true; + }); + + // Allow any original invalidHandler (including WET's) to run with the reduced list. + return origInvalid.call(this, formEl, validator); + }; + } + + /** + * Try to find the jQuery Validate instance created by WET for this form. + * If found, wire our grouping/dedup logic into it. + * Returns true if the validator exists and was wired. + */ + function tryWire() { + const v = $form.data('validator') || $host.data('validator'); + if (v) wire(v); + return !!v; + } + + // If WET's validator is already present, wire immediately. + if (tryWire()) return; + + // Otherwise, wait for WET to finish attaching the validator, then wire. + // 'wb-ready.wb-frmvld' fires when the plugin is ready on this wrapper. + $host.on('wb-ready.wb-frmvld', tryWire); + + // ---- Submission guards -------------------------------------------------- + // Prevent the form from submitting when invalid (covers: + // - clicking the submit button + // - pressing Enter in a field + // - any custom handler that falls back to form.submit()) + // + // We *evaluate* validity using $form.valid()/v.checkForm() and block the + // event if invalid. We also focus the first invalid field for accessibility. + + // Guard the native form submit event. + $form.on('submit.wetGroupOneMessage', function (e) { + const v = $form.data('validator') || $host.data('validator'); + // If no validator, let normal processing happen. + if (!v) return; + + // valid() calls checkForm() and sets up error display without submitting. + const ok = $form.valid ? $form.valid() : v.checkForm(); + if (!ok) { + v.focusInvalid && v.focusInvalid(); + e.preventDefault(); + e.stopImmediatePropagation(); + e.stopPropagation(); + return false; + } + }); + + // Guard submit button clicks as well to catch flows + // where other handlers might try to submit programmatically. + $form.find('button[type=submit], input[type=submit]').on('click.wetGroupOneMessage', function (e) { + const v = $form.data('validator') || $host.data('validator'); + if (!v) return; + const ok = $form.valid ? $form.valid() : v.checkForm(); + if (!ok) { + v.focusInvalid && v.focusInvalid(); + e.preventDefault(); + e.stopImmediatePropagation(); + e.stopPropagation(); + return false; + } + }); + }); + } + }; +})(jQuery, Drupal, once); diff --git a/modules/custom/wxt_ext/wxt_ext_webform/js/webform_required_marker.js b/modules/custom/wxt_ext/wxt_ext_webform/js/webform_required_marker.js new file mode 100644 index 00000000..c6391d53 --- /dev/null +++ b/modules/custom/wxt_ext/wxt_ext_webform/js/webform_required_marker.js @@ -0,0 +1,137 @@ +/** + * @file + * Webform/WET-BOEW required marker enhancer for conditional fields. + * + * Purpose: + * - Mirror runtime toggles of "required" on Webform elements without + * duplicating server-rendered markers. + * - When a field becomes required client-side, append + * as the + * last child of its