Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
209 / 209
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
NameFormSettingsHelperTrait
100.00% covered (success)
100.00%
209 / 209
100.00% covered (success)
100.00%
16 / 16
46
100.00% covered (success)
100.00%
1 / 1
 trustedCallbacks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fieldSettingsFormPreRender
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
1 / 1
6
 validateOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extractAllowedValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolveComponents
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 initFormScaffold
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
3
 routeTableGroupChild
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
4
 buildIndentRow
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 buildStandardRow
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
5
 buildLabelCell
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 buildComponentColumns
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
7
 buildComponentVisibilityStates
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
1
 flushOrphanedGroupedElements
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
6
 appendFootnotes
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 buildColspanContainer
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 buildEmptyCell
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\name\Traits;
6
7use Drupal\Core\Form\FormStateInterface;
8use Drupal\Core\Render\Element;
9use Drupal\name\Utility\NameOptionValidator;
10use Symfony\Component\DependencyInjection\ContainerInterface;
11
12/**
13 * Name settings trait.
14 *
15 * Shared methods to assist handling the field element setting forms.
16 */
17trait NameFormSettingsHelperTrait {
18
19  /**
20   * {@inheritdoc}
21   */
22  public static function trustedCallbacks() {
23    return ['fieldSettingsFormPreRender'];
24  }
25
26  /**
27   * Themes up the field settings into a table.
28   */
29  public function fieldSettingsFormPreRender($form) {
30    [$components, $excluded_components] = $this->resolveComponents($form);
31    $form = $this->initFormScaffold($form, $components, $excluded_components);
32    $grouped_elements = [];
33    $help_footer_notes = [];
34    $footer_notes_counter = 0;
35    $component_count = count($components) - count($excluded_components);
36
37    foreach (Element::children($form) as $child) {
38      if (in_array($child, ['name_settings', 'top', 'hidden'], TRUE)) {
39        continue;
40      }
41
42      if (!empty($form[$child]['#table_group'])) {
43        if ($this->routeTableGroupChild($child, $form, $grouped_elements)) {
44          continue;
45        }
46      }
47      if (!empty($form[$child]['#indent_row'])) {
48        $this->buildIndentRow(
49          $child,
50          $form,
51          $grouped_elements,
52          $component_count,
53        );
54        continue;
55      }
56      $this->buildStandardRow(
57        $child,
58        $form,
59        $components,
60        $excluded_components,
61        $grouped_elements,
62        $help_footer_notes,
63        $footer_notes_counter,
64        $component_count,
65      );
66    }
67
68    $this->flushOrphanedGroupedElements(
69      $form,
70      $grouped_elements,
71      $component_count,
72    );
73    $this->appendFootnotes($form, $help_footer_notes);
74    $form['#sorted'] = FALSE;
75
76    return $form;
77  }
78
79  /**
80   * Helper function to validate minimum components.
81   *
82   * @param array $element
83   *   Element being validated.
84   * @param \Drupal\Core\Form\FormStateInterface $form_state
85   *   The form state.
86   * @param mixed $values
87   *   Values to check.
88   * @param int $max_length
89   *   The max length.
90   */
91  protected static function validateOptions($element, FormStateInterface $form_state, $values, $max_length) {
92    NameOptionValidator::validate($element, $form_state, (array) $values, (int) $max_length);
93  }
94
95  /**
96   * Helper function to get the allowed values.
97   *
98   * @param string $string
99   *   The string to parse.
100   *
101   * @return array
102   *   The parsed values.
103   */
104  protected static function extractAllowedValues($string) {
105    return array_filter(array_map('trim', explode("\n", $string)));
106  }
107
108  /**
109   * Resolves translatable components and excluded components.
110   *
111   * @param array $form
112   *   The current form.
113   *
114   * @return array{0: array, 1: array}
115   *   The components and excluded component list.
116   */
117  private function resolveComponents(array $form): array {
118    $metadata = \Drupal::getContainer()
119      ->get('name.component_metadata', ContainerInterface::NULL_ON_INVALID_REFERENCE);
120    $components = $metadata ? $metadata->getTranslations() : [];
121
122    $excluded_components = !empty($form['#excluded_components'])
123      ? $form['#excluded_components']
124      : [];
125
126    return [$components, $excluded_components];
127  }
128
129  /**
130   * Initializes the settings table wrapper and component headers.
131   *
132   * @param array $form
133   *   The current form.
134   * @param array $components
135   *   Available components.
136   * @param array $excluded_components
137   *   Excluded components.
138   *
139   * @return array
140   *   The initialized form.
141   */
142  private function initFormScaffold(
143    array $form,
144    array $components,
145    array $excluded_components,
146  ): array {
147    $form = [
148      'top' => [],
149      'hidden' => ['#access' => FALSE],
150      'name_settings' => [
151        '#type' => 'container',
152        'table' => [
153          '#type' => 'table',
154          '#header' => [
155            [
156              'data' => $this->t('Field'),
157            ],
158          ],
159          '#weight' => -2,
160        ],
161      ] + ($form['name_settings'] ?? []),
162    ] + $form;
163
164    foreach ($components as $key => $title) {
165      if (empty($excluded_components[$key])) {
166        $form['name_settings']['table']['#header'][] = [
167          'data' => $title,
168        ];
169      }
170    }
171
172    return $form;
173  }
174
175  /**
176   * Routes children that declare #table_group.
177   *
178   * @param string $child
179   *   The child key being processed.
180   * @param array $form
181   *   The current form.
182   * @param array $grouped_elements
183   *   Deferred grouped elements.
184   *
185   * @return bool
186   *   TRUE if the child processing is complete.
187   */
188  private function routeTableGroupChild(
189    string $child,
190    array &$form,
191    array &$grouped_elements,
192  ): bool {
193    $table_group = (string) $form[$child]['#table_group'];
194    if ($table_group === 'none') {
195      return TRUE;
196    }
197
198    if ($table_group === 'above') {
199      $form['top'][$child] = $form[$child];
200      unset($form[$child]);
201      return TRUE;
202    }
203
204    if (!isset($form['name_settings']['table'][$table_group]['elements'])) {
205      $grouped_elements[$table_group][$child] = $form[$child];
206      unset($form[$child]);
207      return TRUE;
208    }
209
210    $form['name_settings']['table'][$table_group]['elements'][$child] = $form[$child];
211    unset($form[$child]);
212
213    return TRUE;
214  }
215
216  /**
217   * Builds a single indented row.
218   *
219   * @param string $child
220   *   The child key being processed.
221   * @param array $form
222   *   The current form.
223   * @param array $grouped_elements
224   *   Deferred grouped elements.
225   * @param int $component_count
226   *   Number of visible components.
227   */
228  private function buildIndentRow(
229    string $child,
230    array &$form,
231    array &$grouped_elements,
232    int $component_count,
233  ): void {
234    $elements_data = $this->buildColspanContainer($component_count)
235      + $form[$child];
236    foreach ($grouped_elements[$child] ?? [] as $grouped_key => $grouped_element) {
237      $elements_data[$grouped_key] = $grouped_element;
238    }
239
240    $form['name_settings']['table'][$child] = [
241      'field' => [
242        '#markup' => '&nbsp;',
243      ],
244      'elements' => $elements_data,
245    ];
246    unset($form[$child]);
247  }
248
249  /**
250   * Builds a standard row with label and component columns.
251   *
252   * @param string $child
253   *   The child key being processed.
254   * @param array $form
255   *   The current form.
256   * @param array $components
257   *   Available components.
258   * @param array $excluded_components
259   *   Excluded components.
260   * @param array $grouped_elements
261   *   Deferred grouped elements.
262   * @param array $help_footer_notes
263   *   Collected footer notes.
264   * @param int $footer_notes_counter
265   *   Footnote counter.
266   * @param int $component_count
267   *   Number of visible components.
268   */
269  private function buildStandardRow(
270    string $child,
271    array &$form,
272    array $components,
273    array $excluded_components,
274    array &$grouped_elements,
275    array &$help_footer_notes,
276    int &$footer_notes_counter,
277    int $component_count,
278  ): void {
279    $child_element = $form[$child];
280    $row = [];
281
282    if (isset($child_element['#title'])) {
283      $row['field'] = $this->buildLabelCell(
284        $child_element,
285        $help_footer_notes,
286        $footer_notes_counter,
287      );
288    }
289
290    $row += $this->buildComponentColumns(
291      $child,
292      $child_element,
293      $components,
294      $excluded_components,
295      $form['hidden'],
296    );
297
298    if (!empty($grouped_elements[$child])) {
299      if (!isset($row['elements'])) {
300        $row['elements'] = $this->buildColspanContainer($component_count);
301      }
302      foreach ($grouped_elements[$child] as $grouped_key => $grouped_element) {
303        $row['elements'][$grouped_key] = $grouped_element;
304      }
305    }
306
307    $form['name_settings']['table'][$child] = $row;
308    unset($form[$child]);
309  }
310
311  /**
312   * Builds the label cell, including a footnote marker if needed.
313   *
314   * @param array $child_element
315   *   The child form element.
316   * @param array $help_footer_notes
317   *   Collected footer notes.
318   * @param int $footer_notes_counter
319   *   Footnote counter.
320   *
321   * @return array
322   *   The label cell.
323   */
324  private function buildLabelCell(
325    array &$child_element,
326    array &$help_footer_notes,
327    int &$footer_notes_counter,
328  ): array {
329    $label_cell = [
330      '#type' => 'container',
331      'title' => [
332        '#plain_text' => (string) $child_element['#title'],
333      ],
334    ];
335
336    if (!empty($child_element['#description'])) {
337      $footer_notes_counter += 1;
338      $footnote_sup = $this->t(
339        '<sup>@number</sup>',
340        ['@number' => $footer_notes_counter],
341      );
342      $label_cell['footnote'] = [
343        '#markup' => $footnote_sup,
344      ];
345      $help_footer_notes[] = $child_element['#description'];
346      unset($child_element['#description']);
347    }
348
349    return $label_cell;
350  }
351
352  /**
353   * Builds all component columns for a standard row.
354   *
355   * @param string $child
356   *   The child key being processed.
357   * @param array $child_element
358   *   The child form element.
359   * @param array $components
360   *   Available components.
361   * @param array $excluded_components
362   *   Excluded components.
363   * @param array $hidden_elements
364   *   Hidden elements keyed by child.
365   *
366   * @return array
367   *   Component columns keyed by component name.
368   */
369  private function buildComponentColumns(
370    string $child,
371    array &$child_element,
372    array $components,
373    array $excluded_components,
374    array &$hidden_elements,
375  ): array {
376    $row = [];
377    $component_keys = array_keys($components);
378    foreach ($component_keys as $weight => $key) {
379      $component_is_excluded = (
380        !empty($excluded_components[$key]) && isset($child_element[$key])
381      );
382      if ($component_is_excluded) {
383        $child_element[$key]['#access'] = FALSE;
384        $hidden_elements[$child][$key] = $child_element[$key];
385        continue;
386      }
387
388      if (!isset($child_element[$key])) {
389        $row[$key] = $this->buildEmptyCell($weight);
390        continue;
391      }
392
393      $child_element[$key]['#attributes']['title'] = $child_element[$key]['#title'];
394      if (($child_element[$key]['#type'] ?? NULL) === 'checkbox') {
395        $child_element[$key]['#title_display'] = 'invisible';
396      }
397      $row[$key] = [
398        '#weight' => $weight,
399      ] + $child_element[$key];
400
401      // Show columns when component is checked or label is blank.
402      if ($child !== 'components') {
403        $row[$key]['#states'] = $this->buildComponentVisibilityStates($key);
404      }
405    }
406
407    return $row;
408  }
409
410  /**
411   * Builds visibility states for non-component rows.
412   *
413   * @param string $key
414   *   Component key.
415   *
416   * @return array
417   *   Render API states definition.
418   */
419  private function buildComponentVisibilityStates(string $key): array {
420    return [
421      'visible' => [
422        [
423          ':input[name$="[components][' . $key . ']"]' => [
424            'checked' => TRUE,
425          ],
426        ],
427        'or',
428        [
429          ':input[name$="[labels][' . $key . ']"]' => [
430            'empty' => TRUE,
431          ],
432        ],
433      ],
434    ];
435  }
436
437  /**
438   * Adds grouped elements that were deferred until after row creation.
439   *
440   * @param array $form
441   *   The current form.
442   * @param array $grouped_elements
443   *   Deferred grouped elements.
444   * @param int $component_count
445   *   Number of visible components.
446   */
447  private function flushOrphanedGroupedElements(
448    array &$form,
449    array $grouped_elements,
450    int $component_count,
451  ): void {
452    foreach ($grouped_elements as $target_key => $elements) {
453      if (isset($form['name_settings']['table'][$target_key]['elements'])) {
454        foreach ($elements as $grouped_key => $grouped_element) {
455          $form['name_settings']['table'][$target_key]['elements'][$grouped_key] = $grouped_element;
456        }
457      }
458      elseif (!isset($form['name_settings']['table'][$target_key])) {
459        $elements_data = $this->buildColspanContainer($component_count);
460        foreach ($elements as $grouped_key => $grouped_element) {
461          $elements_data[$grouped_key] = $grouped_element;
462        }
463        $form['name_settings']['table'][$target_key] = [
464          'field' => [
465            '#markup' => '&nbsp;',
466          ],
467          'elements' => $elements_data,
468        ];
469      }
470    }
471  }
472
473  /**
474   * Appends footnotes if descriptions were collected.
475   *
476   * @param array $form
477   *   The current form.
478   * @param array $help_footer_notes
479   *   Collected footer notes.
480   */
481  private function appendFootnotes(array &$form, array $help_footer_notes): void {
482    if (empty($help_footer_notes)) {
483      return;
484    }
485
486    $form['name_settings']['footnotes'] = [
487      '#type' => 'details',
488      '#title' => t('Footnotes'),
489      '#collapsible' => TRUE,
490      '#collapsed' => TRUE,
491      '#parents' => [],
492      '#weight' => -1,
493      'help_items' => [
494        '#theme' => 'item_list',
495        '#list_type' => 'ol',
496        '#items' => $help_footer_notes,
497      ],
498    ];
499  }
500
501  /**
502   * Builds a colspan-aware container wrapper.
503   *
504   * @param int $component_count
505   *   Number of visible components.
506   *
507   * @return array
508   *   The container render array.
509   */
510  private function buildColspanContainer(int $component_count): array {
511    return [
512      '#type' => 'container',
513      '#wrapper_attributes' => [
514        'colspan' => $component_count,
515      ],
516    ];
517  }
518
519  /**
520   * Builds an empty component placeholder cell.
521   *
522   * @param int $weight
523   *   Display weight.
524   *
525   * @return array
526   *   The placeholder cell.
527   */
528  private function buildEmptyCell(int $weight): array {
529    return [
530      '#markup' => '&nbsp;',
531      '#weight' => $weight,
532    ];
533  }
534
535}