diff --git a/config/schema/webform.third_party.localgov_forms.schema.yml b/config/schema/webform.third_party.localgov_forms.schema.yml new file mode 100644 index 0000000..9b69937 --- /dev/null +++ b/config/schema/webform.third_party.localgov_forms.schema.yml @@ -0,0 +1,7 @@ +webform.admin_settings.third_party.localgov_forms: + type: mapping + label: 'LocalGov Forms settings' + mapping: + mark_optional: + type: boolean + label: 'Add optional to non-required element labels' diff --git a/js/localgov_forms.state.js b/js/localgov_forms.state.js new file mode 100644 index 0000000..8149690 --- /dev/null +++ b/js/localgov_forms.state.js @@ -0,0 +1,20 @@ +/** + * @file + * Additional JavaScript behaviors for webform #states. + */ + +(function ($, Drupal) { + + 'use strict'; + + const $document = $(document); + $document.on('state:required', (e) => { + // Add or remove '(optional)' from element label. + if (e.trigger) { + $(e.target) + .find('span.localgov-form-optional') + .html(e.value ? '' : Drupal.t('(optional)')); + } + }); + +})(jQuery, Drupal); diff --git a/localgov_forms.libraries.yml b/localgov_forms.libraries.yml index da1857b..e06b949 100644 --- a/localgov_forms.libraries.yml +++ b/localgov_forms.libraries.yml @@ -28,3 +28,10 @@ localgov_forms.address_change: - core/drupal - core/once - core/jquery + +localgov_forms.state: + js: + js/localgov_forms.state.js: {} + dependencies: + - core/drupal + - core/jquery diff --git a/localgov_forms.module b/localgov_forms.module index a8f1b00..8767816 100644 --- a/localgov_forms.module +++ b/localgov_forms.module @@ -5,7 +5,10 @@ * Hook implementations. */ +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Hook\Attribute\LegacyHook; use Drupal\Core\Render\Element; +use Drupal\localgov_forms\Hook\ThemeHooks; /** * Implements hook_theme(). @@ -55,3 +58,15 @@ function template_preprocess_localgov_forms_uk_address(array &$variables): void function localgov_forms_preprocess_webform(array &$variables): void { $variables['#attached']['library'][] = 'localgov_forms/localgov_forms.form_errors'; } + +// @phpstan-ignore-next-line +#[LegacyHook] +function localgov_forms_element_info_alter(array &$types): void { + \Drupal::service(ThemeHooks::class)->elementInfoAlter($types); +} + +// @phpstan-ignore-next-line +#[LegacyHook] +function localgov_forms_webform_admin_third_party_settings_form_alter(&$form, FormStateInterface $form_state) { + \Drupal::service(ThemeHooks::class)->webformAdminForm($form, $form_state); +} diff --git a/localgov_forms.services.yml b/localgov_forms.services.yml index 959c86b..08d4df8 100644 --- a/localgov_forms.services.yml +++ b/localgov_forms.services.yml @@ -11,3 +11,13 @@ services: plugin.manager.pii_redactor: class: Drupal\localgov_forms\Plugin\PIIRedactorPluginManager parent: default_plugin_manager + + Drupal\localgov_forms\Hook\ThemeHooks: + class: Drupal\localgov_forms\Hook\ThemeHooks + arguments: ['@webform.third_party_settings_manager'] + + localgov_forms.event_subscriber: + class: Drupal\localgov_forms\EventSubscriber\ConfigEventSubscriber + arguments: ['@plugin.manager.element_info'] + tags: + - { name: event_subscriber } diff --git a/src/Element/AddressLookupElement.php b/src/Element/AddressLookupElement.php index 88dc0f4..1717ab0 100644 --- a/src/Element/AddressLookupElement.php +++ b/src/Element/AddressLookupElement.php @@ -108,7 +108,21 @@ public static function processAddressLookupElement(&$element, FormStateInterface '#attributes' => [ 'class' => ['js-address-searchstring'], ], + // Required validation is done on the element, based on required + // address parts. Display of required status is done on the collection. + '#required' => NULL, ]; + // Display title, description and help on the active element. + $properties = [ + '#title' => '#title', + // phpcs:ignore DrupalPractice.General.DescriptionT.DescriptionT + '#description' => '#description', + '#help' => '#help', + ]; + $element['address_search']['address_searchstring'] = array_merge( + $element['address_search']['address_searchstring'], + array_intersect_key($element, $properties) + ); $element['address_search']['address_actions'] = [ '#type' => 'container', @@ -174,6 +188,7 @@ public static function processAddressLookupElement(&$element, FormStateInterface 'class' => ['js-address-select'], ], '#address_type' => $element['#address_type'] ?? 'residential', + '#required' => NULL, ]; if ($form_state->isProcessingInput()) { diff --git a/src/Element/UKAddressLookup.php b/src/Element/UKAddressLookup.php index 5850ef4..a8eb333 100644 --- a/src/Element/UKAddressLookup.php +++ b/src/Element/UKAddressLookup.php @@ -47,6 +47,7 @@ public static function getCompositeElements(array $element) { $element_list = []; $element_list['address_lookup'] = [ '#type' => 'localgov_forms_address_lookup', + '#title' => $element['#title'] ?? NULL, '#address_type' => $element['#address_type'] ?? 'residential', '#address_search_description' => $element['#address_search_description'] ?? NULL, '#address_select_title' => $element['#address_select_title'] ?? NULL, @@ -69,6 +70,7 @@ public static function getCompositeElements(array $element) { foreach ($extra_elements as $extra_element) { $element_list[$extra_element] = [ '#type' => 'hidden', + '#title' => $extra_element, '#default_value' => '', '#attributes' => [ 'class' => ['js-localgov-forms-webform-uk-address--' . $extra_element], @@ -76,8 +78,8 @@ public static function getCompositeElements(array $element) { ]; } - $element_list['#attached']['library'][] = 'localgov_forms/localgov_forms.address_select'; - $element_list['#attached']['drupalSettings']['centralHub']['isManualAddressEntryBtnAlwaysVisible'] = isset($element['#always_display_manual_address_entry_btn']) ? ($element['#always_display_manual_address_entry_btn'] === 'yes') : TRUE; + $element_list['address_lookup']['#attached']['library'][] = 'localgov_forms/localgov_forms.address_select'; + $element_list['address_lookup']['#attached']['drupalSettings']['centralHub']['isManualAddressEntryBtnAlwaysVisible'] = isset($element['#always_display_manual_address_entry_btn']) ? ($element['#always_display_manual_address_entry_btn'] === 'yes') : TRUE; return $element_list; } diff --git a/src/EventSubscriber/ConfigEventSubscriber.php b/src/EventSubscriber/ConfigEventSubscriber.php new file mode 100644 index 0000000..7be31ba --- /dev/null +++ b/src/EventSubscriber/ConfigEventSubscriber.php @@ -0,0 +1,49 @@ +getConfig()) && + ($config->getName() == 'webform.settings') && + $event->isChanged('third_party_settings.localgov_forms.mark_optional') + ) { + $this->pluginManagerElementInfo->clearCachedDefinitions(); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + return [ + ConfigEvents::SAVE => 'onConfigSave', + ]; + } + +} diff --git a/src/Hook/ThemeHooks.php b/src/Hook/ThemeHooks.php new file mode 100644 index 0000000..07f983b --- /dev/null +++ b/src/Hook/ThemeHooks.php @@ -0,0 +1,120 @@ + 'details', + '#title' => new TranslatableMarkup('LocalGov Forms'), + ]; + $form['third_party_settings']['localgov_forms']['mark_optional'] = [ + '#type' => 'checkbox', + '#title' => new TranslatableMarkup("Add '(optional)' to non-required elements"), + '#description' => new TranslatableMarkup('If checked GDS forms style addition to the label title.'), + '#default_value' => $this->webformThirdPartySettings->getThirdPartySetting('localgov_forms', 'mark_optional') ?: FALSE, + ]; + } + + #[Hook('element_info_alter')] + public function elementInfoAlter(array &$types): void { + if ($this->webformThirdPartySettings->getThirdPartySetting('localgov_forms', 'mark_optional') ?: FALSE) { + foreach ($types as $type => $info) { + if (($info['#input'] ?? FALSE) && !in_array($type, static::$skipTypes, TRUE)) { + $types[$type]['#after_build'][] = [static::class, 'optionalElement']; + } + } + } + } + + /** + * After build callback. + * + * Add '(optional)' to appropriate non-required element titles. + * + * @param array $element + * The form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The, potentially altered, form element. + */ + static function optionalElement(array $element, FormStateInterface $form_state): array { + if ($form_state->getFormObject() instanceof WebformSubmissionForm) { + $type = $element['#type']; + if ($type === 'checkbox' || $type === 'radio') { + // If it is desired to add optional to single checkboxes there will be + // a single parent with the same name as the checkbox in #parents. + // A checkbox in a checkboxes list will have at least two parents. + return $element; + } + + $form = $form_state->getCompleteForm(); + $parents = $element['#array_parents']; + array_pop($parents); + $parent = NestedArray::getValue($form, $parents); + $parent_type = $parent['#type']; + + // Don't show optional on every field if whole address optional. + if ($parent_type === 'localgov_webform_uk_address') { + $all_optional = TRUE; + foreach (Element::children($parent) as $sibling_name) { + $sibling = $parent[$sibling_name]; + if ($sibling['#access'] && isset($sibling['#required']) && $sibling['#required']) { + $all_optional = FALSE; + break; + } + } + if ($all_optional) { + return $element; + } + } + + // Seems conditionally required will trigger this, + // if default required, it's then disable with JS. + if ( + $element['#required'] === FALSE && + isset($element['#title']) + ) { + $element['#title'] .= ' ' + . new TranslatableMarkup('(optional)') + . ''; + $element['#attached']['library'][] = 'localgov_forms/localgov_forms.state'; + } + } + + return $element; + } + +}