Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/schema/webform.third_party.localgov_forms.schema.yml
Original file line number Diff line number Diff line change
@@ -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'
20 changes: 20 additions & 0 deletions js/localgov_forms.state.js
Original file line number Diff line number Diff line change
@@ -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);
7 changes: 7 additions & 0 deletions localgov_forms.libraries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
15 changes: 15 additions & 0 deletions localgov_forms.module
Original file line number Diff line number Diff line change
Expand Up @@ -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().
Expand Down Expand Up @@ -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);
}
10 changes: 10 additions & 0 deletions localgov_forms.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
15 changes: 15 additions & 0 deletions src/Element/AddressLookupElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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()) {
Expand Down
6 changes: 4 additions & 2 deletions src/Element/UKAddressLookup.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -69,15 +70,16 @@ 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],
],
];
}

$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;
}
Expand Down
49 changes: 49 additions & 0 deletions src/EventSubscriber/ConfigEventSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Drupal\localgov_forms\EventSubscriber;

use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
* React to config changes.
*/
final class ConfigEventSubscriber implements EventSubscriberInterface {

/**
* Constructs a ConfigEventSubscriber object.
*/
public function __construct(
private readonly ElementInfoManagerInterface $pluginManagerElementInfo,
) {}

/**
* Config saved.
*
* Clear element info cache if mark optional configuration changed.
*
* @see \Drupal\localgov_forms\Hook\ThemeHooks
*/
public function onConfigSave(ConfigCrudEvent $event): void {
if (($config = $event->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',
];
}

}
120 changes: 120 additions & 0 deletions src/Hook/ThemeHooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

declare(strict_types=1);

namespace Drupal\localgov_forms\Hook;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\webform\WebformSubmissionForm;
use Drupal\webform\WebformThirdPartySettingsManager;

/**
* Theme related hooks.
*/
class ThemeHooks {

/**
* @var array
* Input element types not to attach to.
*/
public static array $skipTypes = [];

/**
* Construct a new class.
*
* @param \Drupal\webform\WebformThirdPartySettingsManager $webformThirdPartySettings
* Webform third party settings manager.
*/
public function __construct(protected WebformThirdPartySettingsManager $webformThirdPartySettings) {
}

#[Hook('webform_admin_third_party_settings_form_alter')]
public function webformAdminForm(&$form, FormStateInterface $form_state) {
$form['third_party_settings']['localgov_forms'] = [
'#type' => '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'] .= ' <span class="localgov-form-optional">'
. new TranslatableMarkup('(optional)')
. '</span>';
$element['#attached']['library'][] = 'localgov_forms/localgov_forms.state';
}
}

return $element;
}

}
Loading