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 + * (required) as the + * last child of its or . Remove it when no longer required. + */ +(function (Drupal, once) { + 'use strict'; + + const OUR_ATTR = 'data-webform-required-marker'; + const OUR_SEL = `strong.required[${OUR_ATTR}="1"]`; + + // Track prior required state per element (label/legend). + const baseline = new WeakMap(); + + // Helpers + function isLabelRequired(label) { + return label.classList.contains('js-form-required'); + } + function isLegendRequired(legend) { + return !!legend.querySelector('span.js-form-required'); + } + // Place marker before any inline error badge (e.g., .label.label-danger). + function ensureBeforeError(el, node) { + const err = el.querySelector('strong.error'); + const errIsChild = err && err.parentNode === el; + if (errIsChild) { + if (node.parentNode !== el || node.nextElementSibling !== err) { + el.insertBefore(node, err); + } + } else { + if (node.parentNode !== el || el.lastElementChild !== node) { + el.appendChild(node); + } + } + } + function addMarker(el) { + el.classList.add('required'); + let strong = el.querySelector(OUR_SEL); + if (!strong) { + strong = document.createElement('strong'); + strong.className = 'required'; + strong.setAttribute(OUR_ATTR, '1'); + strong.setAttribute('aria-hidden', 'true'); + strong.textContent = `(${Drupal.t('required')})`; + } + ensureBeforeError(el, strong); + } + function removeMarker(el) { + el.classList.remove('required'); + const ours = el.querySelector(OUR_SEL); + if (ours) ours.remove(); + } + + // React to a possible state change on a label/legend, + // but only if baseline exists. + function syncWithTransition(el, nowRequired) { + if (!baseline.has(el)) { + // First time we see this element post-attach: + // set baseline, do nothing. + baseline.set(el, nowRequired); + return; + } + const wasRequired = baseline.get(el); + if (wasRequired === nowRequired) { + // No change. + return; + } + // Update baseline then act. + baseline.set(el, nowRequired); + if (nowRequired) addMarker(el); + else removeMarker(el); + } + + Drupal.behaviors.webformRequiredMarkerObserver = { + attach(context) { + // Scope to .wb-frmvld (your library is only attached + // when inline validation is enabled). + once('webform-required-marker-observer', '.wb-frmvld', context).forEach((root) => { + // Defer baseline capture to the next frame so we + // don't react to initial build churn. + requestAnimationFrame(() => { + // Capture baseline for all labels and legends without touching DOM. + root.querySelectorAll('label').forEach((label) => { + baseline.set(label, isLabelRequired(label)); + }); + root.querySelectorAll('legend').forEach((legend) => { + baseline.set(legend, isLegendRequired(legend)); + }); + }); + + // Observe only class/child mutations; + // act *after* baseline exists. + const observer = new MutationObserver((mutations) => { + for (const m of mutations) { + // Class toggles on . + if (m.type === 'attributes' && m.attributeName === 'class' && m.target.tagName === 'LABEL') { + const label = /** @type {HTMLElement} */ (m.target); + syncWithTransition(label, isLabelRequired(label)); + continue; + } + + // Class toggles on inside . + if (m.type === 'attributes' && m.attributeName === 'class' && m.target.tagName === 'SPAN') { + const legend = m.target.closest('legend'); + if (legend) { + syncWithTransition(legend, isLegendRequired(legend)); + } + continue; + } + + // Child list changes within/under a (some UIs replace nodes). + if (m.type === 'childList') { + const legend = (m.target.closest && m.target.closest('legend')) || (m.target.tagName === 'LEGEND' ? m.target : null); + if (legend) { + syncWithTransition(legend, isLegendRequired(legend)); + } + } + } + }); + + observer.observe(root, { + subtree: true, + attributes: true, + attributeFilter: ['class'], + attributeOldValue: true, + childList: true, + }); + }); + } + }; +})(Drupal, once); diff --git a/modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.libraries.yml b/modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.libraries.yml new file mode 100644 index 00000000..dde59c52 --- /dev/null +++ b/modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.libraries.yml @@ -0,0 +1,8 @@ +webform_required_marker: + js: + js/webform_required_marker.js: {} + dependencies: + - core/drupal + - core/once + - core/drupalSettings + diff --git a/modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.module b/modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.module index 73f71d6a..ef6ce0d9 100644 --- a/modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.module +++ b/modules/custom/wxt_ext/wxt_ext_webform/wxt_ext_webform.module @@ -9,6 +9,9 @@ use Drupal\Core\Link; use Drupal\webform\Entity\Webform; use Drupal\webform\Entity\WebformSubmission; +use Drupal\webform\Utility\WebformArrayHelper; +use Drupal\webform\Utility\WebformElementHelper; +use Drupal\webform\WebformInterface; /** * Implements hook_theme(). @@ -28,6 +31,71 @@ function wxt_ext_webform_theme($existing, $type, $theme, $path) { ]; } +/** + * Implements hook_form_FORM_ID_alter() for webform_edit_form. + */ +function wxt_ext_webform_form_webform_edit_form_alter(array &$form, FormStateInterface $form_state, $form_id): void { + // Get the Webform entity from the form. + $form_object = $form_state->getFormObject(); + if (!method_exists($form_object, 'getEntity')) { + return; + } + $webform = $form_object->getEntity(); + if (!$webform instanceof WebformInterface) { + return; + } + + $enabled = (bool) $webform->getThirdPartySetting('wxt_ext_webform', 'wet_inline_validation', FALSE); + + if ($enabled) { + \Drupal::messenger()->addStatus(t('WET-BOEW inline validation is ENABLED for this webform. To add special validation to a field, edit the field, go to the Advanced tab -> Element attributes -> Element custom attributes (YAML) and add custom attributes as per https://wet-boew.github.io/wet-boew/docs/ref/formvalid/formvalid-en.html#SpecializedValidation')); + } + else { + \Drupal::messenger()->addWarning(t('WET-BOEW inline validation is DISABLED for this webform. To enable it, go to the Settings tab and select the "Enable WET-BOEW inline validation" option.')); + } +} + +/** + * Implements hook_webform_third_party_settings_form_alter(). + */ +function wxt_ext_webform_webform_third_party_settings_form_alter(array &$form, FormStateInterface $form_state) { + // Get the Webform entity being edited. + $form_object = $form_state->getFormObject(); + $webform = $form_object->getEntity(); + + $form['form']['wxt_settings'] = [ + '#type' => 'details', + '#title' => t('WxT settings'), + '#open' => TRUE, + ]; + + // Add the checkbox and store under third_party_settings. + $form['form']['wxt_settings']['wxt_inline_validation'] = [ + '#type' => 'checkbox', + '#title' => t('Enable WET-BOEW inline validation'), + '#description' => t('Enable WET-BOEW inline form validation for this webform. When enabled, the form will be wrapped in a <div class="wb-frmvld"> container so WET-BOEW can apply its built-in client-side validation, displaying error messages inline as users complete the form.'), + '#default_value' => $webform->getThirdPartySetting('wxt_ext_webform', 'wet_inline_validation', FALSE), + // Make it land in $form_state values under third_party_settings. + '#parents' => ['third_party_settings', 'wxt_ext_webform', 'wet_inline_validation'], + '#weight' => 99, + ]; +} + +/** + * Implements hook_preprocess_form_element(). + */ +function wxt_ext_webform_preprocess_form_element(array &$variables) { + // Only perform this action on webform elements. + if (!WebformElementHelper::isWebformElement($variables['element'])) { + return; + } + + // Remove form-inline class for element wrappers to ensure proper rendering with label. + if (!empty($variables['attributes']['class'])) { + WebformArrayHelper::removeValue($variables['attributes']['class'], 'form-inline'); + } +} + /** * Implements hook_theme_suggestions_HOOK_alter(). */ @@ -86,6 +154,19 @@ function wxt_ext_webform_webform_submission_form_alter(array &$form, FormStateIn ]; } } + + // Get the Webform entity being edited. + $form_object = $form_state->getFormObject(); + $webform_submission = $form_object->getEntity(); + $webform = $webform_submission->getWebform(); + + if ($webform->getThirdPartySetting('wxt_ext_webform', 'wet_inline_validation', FALSE)) { + $form['#prefix'] = ($form['#prefix'] ?? '') . ''; + $form['#suffix'] = '' . ($form['#suffix'] ?? ''); + + // Attach label/required enhancer only when inline validation is enabled. + $form['#attached']['library'][] = 'wxt_ext_webform/webform_required_marker'; + } } /**
<div class="wb-frmvld">