From d4d936daa93f141e6af4e9adde5773364b2e8d4e Mon Sep 17 00:00:00 2001 From: ekes Date: Fri, 7 Mar 2025 14:56:30 +0000 Subject: [PATCH 1/2] Inital port of the Review Element from Tanc @ OpenCode for Greenwich. --- css/localgov_forms_review_element.css | 41 +++ localgov_forms.libraries.yml | 8 + localgov_forms.module | 3 + src/Element/ReviewElement.php | 103 ++++++ src/Plugin/WebformElement/ReviewElement.php | 345 ++++++++++++++++++ .../localgov-forms-review-element.html.twig | 23 ++ 6 files changed, 523 insertions(+) create mode 100644 css/localgov_forms_review_element.css create mode 100644 src/Element/ReviewElement.php create mode 100644 src/Plugin/WebformElement/ReviewElement.php create mode 100644 templates/localgov-forms-review-element.html.twig diff --git a/css/localgov_forms_review_element.css b/css/localgov_forms_review_element.css new file mode 100644 index 0000000..f24e1b2 --- /dev/null +++ b/css/localgov_forms_review_element.css @@ -0,0 +1,41 @@ +.webform-localgov-review-element { + display: flex; + margin-bottom: var(--spacing); + padding-bottom: var(--spacing); + border-bottom: solid var(--border-width) var(--border-color); +} + +.webform-localgov-review-element__label { + flex-basis: 40%; + padding-right: var(--spacing); +} + +.webform-localgov-review-element__title { + font-weight: bold; +} + +.webform-localgov-review-element__value { + flex-basis: 40%; +} + +.webform-localgov-review-element__change { + flex-basis: 20%; + text-align: right; +} + +.webform-localgov-review-element__change input[type="submit"] { + margin: 0; + padding: 0 0 0 var(--spacing); + text-align: right; + text-decoration: underline; + color: var(--color-link); + border: none; + background-color: transparent; + font-size: var(--font-size-medium); +} + +.webform-localgov-review-element__value ul { + margin: 0; + padding: 0; + list-style: none; +} diff --git a/localgov_forms.libraries.yml b/localgov_forms.libraries.yml index d8f347c..95e1575 100644 --- a/localgov_forms.libraries.yml +++ b/localgov_forms.libraries.yml @@ -6,6 +6,14 @@ localgov_forms_uk_address: dependencies: - webform/webform.composite +localgov_forms_review_element: + version: VERSION + css: + theme: + css/localgov_forms_review_element.css: {} + dependencies: + - webform/webform.composite + localgov_forms.form_errors: version: VERSION js: diff --git a/localgov_forms.module b/localgov_forms.module index a8f1b00..9ed4a6a 100644 --- a/localgov_forms.module +++ b/localgov_forms.module @@ -20,6 +20,9 @@ function localgov_forms_theme(): array { 'localgov_forms_uk_address' => [ 'render element' => 'element', ], + 'localgov_forms_review_element' => [ + 'render element' => 'element', + ], ]; } diff --git a/src/Element/ReviewElement.php b/src/Element/ReviewElement.php new file mode 100644 index 0000000..ef6b3a4 --- /dev/null +++ b/src/Element/ReviewElement.php @@ -0,0 +1,103 @@ + [ + [$class, 'processWebformActions'], + [$class, 'processContainer'], + ], + '#theme_wrappers' => ['container'], + ]; + } + + /** + * Processes a form actions container element. + */ + public static function processWebformActions(&$element, FormStateInterface $form_state, &$complete_form) { + if (empty($element['#value'])) { + return $element; + } + + $prefix = ($element['#webform_key']) ? 'edit-' . $element['#webform_key'] . '-' : ''; + + $element['#attributes']['class'][] = 'preview-wrapper'; + // Copy the form's actions to this element. + $element += $complete_form['actions']; + + foreach (static::$buttons as $button_name) { + // Make sure the button exists. + if (!isset($element[$button_name])) { + continue; + } + + // Get settings name. + $settings_name = $button_name; + + // Set unique id for each button. + if ($prefix) { + $element[$button_name]['#id'] = Html::getUniqueId("$prefix$button_name"); + } + + // Hide buttons using #access. + if (!empty($element['#' . $settings_name . '_hide'])) { + $element[$button_name]['#access'] = FALSE; + } + + // Apply custom label. + $element[$button_name]['#value'] = $element['#' . $settings_name . '__label'] ?? 'Change'; + + // The #name attribute needs to be unique so triggeringElement can be + // correctly associated with this button. + $element[$button_name]['#name'] = $prefix . $button_name; + $element[$button_name]['#identifier'] = 'change_wizard_prev'; + + // Apply attributes (class, style, properties). + if (!empty($element['#' . $settings_name . '__attributes'])) { + $element[$button_name] += ['#attributes' => []]; + foreach ($element['#' . $settings_name . '__attributes'] as $attribute_name => $attribute_value) { + if ($attribute_name === 'class') { + $element[$button_name]['#attributes'] += ['class' => []]; + // Merge class names. + $element[$button_name]['#attributes']['class'] = array_merge($element[$button_name]['#attributes']['class'], $attribute_value); + } + else { + $element[$button_name]['#attributes'][$attribute_name] = $attribute_value; + } + } + } + + if (isset($element['#source_page'])) { + $element[$button_name]['#page'] = $element['#source_page']; + } + } + + return $element; + } + +} diff --git a/src/Plugin/WebformElement/ReviewElement.php b/src/Plugin/WebformElement/ReviewElement.php new file mode 100644 index 0000000..029e95f --- /dev/null +++ b/src/Plugin/WebformElement/ReviewElement.php @@ -0,0 +1,345 @@ + '', + 'description' => '', + 'source_component' => '', + 'source_link' => '', + 'wizard_prev_hide' => FALSE, + 'wizard_prev__label' => 'Change', + 'wizard_prev__attributes' => [], + ]; + + $properties += $this->defineDefaultBaseProperties(); + + unset($properties['#wrapper_attributes']); + return $properties; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + $form['custom_preview'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Custom Preview settings'), + ]; + $form['custom_preview']['source_component'] = [ + '#type' => 'textfield', + '#title' => $this->t('Source Component'), + '#description' => $this->t('Enter the machine name of the component(s) to display value(s) from. Use a comma separated list of components to show more than one.'), + '#required' => TRUE, + ]; + $form['custom_preview']['source_link'] = [ + '#type' => 'textfield', + '#title' => $this->t('Source component to link to'), + '#description' => $this->t('Enter the machine name of the component or page to link to with the change/back link. Leave blank to automatically calculate it.'), + '#required' => FALSE, + ]; + $name = 'wizard_prev'; + $form[$name . '_settings'] = [ + '#type' => 'details', + '#open' => TRUE, + '#weight' => -10, + '#title' => $this->t('Change button'), + ]; + $form[$name . '_settings']['description'] = [ + '#markup' => '

' . $this->t('Optionally modify the change button') . '

', + '#access' => TRUE, + ]; + $form[$name . '_settings'][$name . '_hide'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Hide change button'), + '#return_value' => TRUE, + ]; + $form[$name . '_settings'][$name . '__label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Change button label'), + '#description' => $this->t('Defaults to: %value', ['%value' => 'Change']), + '#size' => 20, + '#attributes' => [ + // Make sure default value is never cleared by #states API. + // @see js/webform.states.js + 'data-webform-states-no-clear' => TRUE, + ], + '#states' => [ + 'visible' => [':input[name="properties[' . $name . '_hide]"]' => ['checked' => FALSE]], + ], + ]; + $form[$name . '_settings'][$name . '__attributes'] = [ + '#type' => 'webform_element_attributes', + '#title' => $this->t('Change'), + '#classes' => $this->configFactory->get('webform.settings')->get('settings.button_classes'), + '#states' => [ + 'visible' => [':input[name="properties[' . $name . '_hide]"]' => ['checked' => FALSE]], + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function prepare(array &$element, ?WebformSubmissionInterface $webform_submission = NULL) { + parent::prepare($element, $webform_submission); + + if ($webform_submission) { + // Split the source component field into an array if it is a + // comma separated list. + $source_components = explode(',', $element['#source_component']); + $results = []; + + // Iterate over the source_component array and build the formatted output. + foreach ($source_components as $source_component) { + $source_element = $webform_submission->getWebform()->getElement(trim($source_component)); + + if ($source_element) { + if ($source_element['#type'] === 'webform_wizard_page') { + $formatted_output = $this->getWizardPageElementsOutput($source_element); + if (!empty($formatted_output)) { + $results[] = [ + 'output' => $formatted_output, + 'type' => $source_element['#type'], + 'source_page' => $source_component, + ]; + } + } + else { + $formatted_output = $this->getElementOutput($source_element); + if (!empty($formatted_output)) { + $webform = $webform_submission->getWebform(); + $results[] = [ + 'output' => $formatted_output, + 'type' => $source_element['#type'], + 'source_page' => $this->findSourceComponentPage($webform, $source_component), + ]; + } + } + } + + } + + if (empty($results)) { + return; + } + + if (!empty($element['#source_link'])) { + $source_element = $webform_submission->getWebform()->getElement(trim($element['#source_link'])); + if ($source_element) { + $webform = $webform_submission->getWebform(); + $source_page_link = $source_element['#type'] === 'webform_wizard_page' ? $element['#source_link'] : $this->findSourceComponentPage($webform, $source_component); + } + } + else { + $source_page_link = reset($results)['source_page']; + } + + if ($results > 1) { + // Make a new array containing only 'output'. + $items = array_column($results, 'output'); + // Output results in an unordered list. + $compiled_output = [ + '#theme' => 'item_list', + '#list_type' => 'ul', + '#items' => $items, + ]; + } + else { + $compiled_output = $results[0]['output']; + } + + // Assign the rendered output to the custom preview element. + $element['#label'] = $element['#title'] ?? $source_element['#title']; + $element['#theme'] = 'localgov_forms_review_element'; + $element['#value'] = $compiled_output; + + if ($source_page_link) { + $element['#source_page'] = $source_page_link; + } + + } + } + + /** + * Gets the rendered output of the source element. + * + * @param array $source_element + * The source element. + * + * @return string + * The rendered output of the source element. + */ + private function getElementOutput(array $source_element) { + $webform_submission = $this->getWebformSubmission(); + + // Render the source component using its formatHtml method. + $plugin_manager = \Drupal::service('plugin.manager.webform.element'); + $source_component_plugin = $plugin_manager->createInstance($source_element['#type']); + $formatted_output = $source_component_plugin->formatHtml($source_element, $webform_submission, []); + + return $formatted_output; + } + + /** + * Gets the rendered output of all source elements in a wizard page. + * + * @param array $page_element + * The wizard page element. + * + * @return array|null + * An array of rendered output of all source elements in the wizard page. + */ + private function getWizardPageElementsOutput(array $page_element) { + $webform_submission = $this->getWebformSubmission(); + $element_items = []; + + foreach ($page_element['#webform_children'] as $source_component) { + $source_element = $webform_submission->getWebform()->getElement($source_component); + $element_output = $this->getElementOutput($source_element); + if (!empty($element_output)) { + $element_items[] = $element_output; + } + } + + if (!empty($element_items)) { + return [ + '#theme' => 'item_list', + '#list_type' => 'ul', + '#items' => $element_items, + ]; + } + + return NULL; + } + + /** + * Helper function to find the page containing the source component. + * + * @param \Drupal\webform\WebformInterface $webform + * The webform. + * @param string $source_component + * The source component key. + * + * @return string|null + * The page key containing the source component, or NULL if not found. + */ + private function findSourceComponentPage(WebformInterface $webform, $source_component) { + $elements = $webform->getElementsInitialized(); + + // If there are no wizard pages, the component is on the main form. + if (!$this->hasWizardPages($elements)) { + return ''; + } + + foreach ($elements as $key => $element) { + if (isset($element['#type']) && $element['#type'] === 'webform_wizard_page') { + if ($this->pageContainsComponent($element, $source_component)) { + return $key; + } + } + } + + return NULL; + } + + /** + * Check if the webform has wizard pages. + * + * @param array $elements + * The webform elements. + * + * @return bool + * TRUE if the webform has wizard pages, FALSE otherwise. + */ + private function hasWizardPages(array $elements) { + foreach ($elements as $element) { + if (isset($element['#type']) && $element['#type'] === 'webform_wizard_page') { + return TRUE; + } + } + return FALSE; + } + + /** + * Check if a page contains the source component. + * + * @param array $page_element + * The page element to check. + * @param string $source_component + * The key of the source component. + * + * @return bool + * TRUE if the page contains the component, FALSE otherwise. + */ + private function pageContainsComponent(array $page_element, $source_component) { + if (isset($page_element[$source_component])) { + return TRUE; + } + + foreach ($page_element as $element) { + if (is_array($element) && $this->pageContainsComponent($element, $source_component)) { + return TRUE; + } + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function formatHtmlItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) { + // Should not display any output. + return []; + } + + /** + * {@inheritdoc} + */ + public function formatTextItem(array $element, WebformSubmissionInterface $webform_submission, array $options = []) { + // Should not display any output. + return []; + } + + /** + * {@inheritdoc} + */ + public function isContainer(array $element) { + return TRUE; + } + +} diff --git a/templates/localgov-forms-review-element.html.twig b/templates/localgov-forms-review-element.html.twig new file mode 100644 index 0000000..414b9f9 --- /dev/null +++ b/templates/localgov-forms-review-element.html.twig @@ -0,0 +1,23 @@ +{# +/** + * @file + * Default theme implementation for the custom preview component. + * + * Available variables: + * - label: The display label. + * - value: The value to display. + */ +#} +{{ attach_library('localgov_forms/localgov_forms_review_element') }} +
+
+
{{ element['#label'] }}
+ {% if element['#description'] %} +
{{ element['#description'] }}
+ {% endif %} +
+
{{ element['#value'] }}
+ {% if element.wizard_prev %} +
{{ element.wizard_prev }}
+ {% endif %} +
From ce1367e725251680f05cea52ef4e8bf272812bb4 Mon Sep 17 00:00:00 2001 From: ekes Date: Fri, 7 Mar 2025 15:23:33 +0000 Subject: [PATCH 2/2] Prevent nesting the elements, it doesn't make sense. --- src/Plugin/WebformElement/ReviewElement.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/WebformElement/ReviewElement.php b/src/Plugin/WebformElement/ReviewElement.php index 029e95f..a61f9da 100644 --- a/src/Plugin/WebformElement/ReviewElement.php +++ b/src/Plugin/WebformElement/ReviewElement.php @@ -339,7 +339,7 @@ public function formatTextItem(array $element, WebformSubmissionInterface $webfo * {@inheritdoc} */ public function isContainer(array $element) { - return TRUE; + return FALSE; } }