Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
361 / 361
100.00% covered (success)
100.00%
12 / 12
CRAP
100.00% covered (success)
100.00%
1 / 1
NameFieldSettingsTrait
100.00% covered (success)
100.00%
361 / 361
100.00% covered (success)
100.00%
12 / 12
27
100.00% covered (success)
100.00%
1 / 1
 getDefaultNameFieldSettings
100.00% covered (success)
100.00%
97 / 97
100.00% covered (success)
100.00%
1 / 1
1
 buildNameFieldComponentCheckboxElements
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
3
 buildNameFieldPerComponentElements
100.00% covered (success)
100.00%
54 / 54
100.00% covered (success)
100.00%
1 / 1
2
 filterAutocompleteSourceOptions
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 filterFieldTypeOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 buildNameFieldAutocompleteMatchElements
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
1 / 1
2
 buildNameFieldOptionsTextareaElements
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
3
 buildNameFieldLayoutElement
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultNameFieldSettingsForm
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 validateMinimumComponents
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
5
 validateTitleOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 validateGenerationalOptions
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\Link;
9use Drupal\Core\Url;
10use Drupal\name\Service\NameComponentMetadataService;
11use Symfony\Component\DependencyInjection\ContainerInterface;
12
13/**
14 * Name settings trait.
15 *
16 * Used for handling the core field settings.
17 */
18trait NameFieldSettingsTrait {
19
20  /**
21   * Gets the default settings for controlling a name element.
22   *
23   * @return array
24   *   Default settings.
25   */
26  protected static function getDefaultNameFieldSettings() {
27    return [
28      'components' => [
29        'title' => TRUE,
30        'given' => TRUE,
31        'middle' => TRUE,
32        'family' => TRUE,
33        'generational' => TRUE,
34        'credentials' => TRUE,
35      ],
36      'minimum_components' => [
37        'title' => FALSE,
38        'given' => TRUE,
39        'middle' => FALSE,
40        'family' => TRUE,
41        'generational' => FALSE,
42        'credentials' => FALSE,
43      ],
44      'allow_family_or_given' => FALSE,
45      'max_length' => [
46        'title' => 31,
47        'given' => 63,
48        'middle' => 127,
49        'family' => 63,
50        'generational' => 15,
51        'credentials' => 255,
52      ],
53      'field_type' => [
54        'title' => 'select',
55        'given' => 'text',
56        'middle' => 'text',
57        'family' => 'text',
58        'generational' => 'select',
59        'credentials' => 'text',
60      ],
61      'autocomplete_source' => [
62        'title' => [
63          'title',
64        ],
65        'given' => [],
66        'middle' => [],
67        'family' => [],
68        'generational' => [
69          'generation',
70        ],
71        'credentials' => [],
72      ],
73      'autocomplete_separator' => [
74        'title' => ' ',
75        'given' => ' -',
76        'middle' => ' -',
77        'family' => ' -',
78        'generational' => ' ',
79        'credentials' => ', ',
80      ],
81      'autocomplete_match' => 'starts_with',
82      'autocomplete_match_overrides' => [
83        'title' => '',
84        'given' => '',
85        'middle' => '',
86        'family' => '',
87        'generational' => '',
88        'credentials' => '',
89      ],
90      'title_options' => [
91        t('-- --'),
92        t('Mr.'),
93        t('Mrs.'),
94        t('Miss'),
95        t('Ms.'),
96        t('Dr.'),
97        t('Prof.'),
98      ],
99      'generational_options' => [
100        t('-- --'),
101        t('Jr.'),
102        t('Sr.'),
103        t('I'),
104        t('II'),
105        t('III'),
106        t('IV'),
107        t('V'),
108        t('VI'),
109        t('VII'),
110        t('VIII'),
111        t('IX'),
112        t('X'),
113      ],
114      'sort_options' => [
115        'title' => FALSE,
116        'given' => FALSE,
117        'middle' => FALSE,
118        'family' => FALSE,
119        'generational' => FALSE,
120        'credentials' => FALSE,
121      ],
122      'component_layout' => 'default',
123    ];
124  }
125
126  /**
127   * Builds component checkbox elements for field settings.
128   *
129   * @param array $settings
130   *   The field settings.
131   * @param array $components
132   *   Translated component labels.
133   * @param \Drupal\name\Service\NameComponentMetadataService|null $metadata
134   *   Component metadata service, if available.
135   *
136   * @return array
137   *   Form elements for component selection and sorting.
138   */
139  private function buildNameFieldComponentCheckboxElements(array $settings, array $components, ?NameComponentMetadataService $metadata): array {
140    $element = [];
141    $element['components'] = [
142      '#type' => 'checkboxes',
143      '#title' => $this->t('Components'),
144      '#default_value' => array_keys(array_filter($settings['components'])),
145      '#required' => TRUE,
146      '#description' => $this->t('Only selected components will be activated on this field. All non-selected components / component settings will be ignored.'),
147      '#options' => $components,
148    ];
149    $element['minimum_components'] = [
150      '#type' => 'checkboxes',
151      '#title' => $this->t('Minimum components'),
152      '#default_value' => array_keys(array_filter($settings['minimum_components'])),
153      '#required' => TRUE,
154      '#description' => $this->t('The minimal set of components required before the field is considered completed enough to save.'),
155      '#options' => $components,
156      '#element_validate' => [[get_class($this), 'validateMinimumComponents']],
157    ];
158    $element['components_extra'] = [
159      '#indent_row' => TRUE,
160    ];
161    $element['allow_family_or_given'] = [
162      '#type' => 'checkbox',
163      '#title' => $this->t('Allow a single valid given or family value to fulfill the minimum component requirements for both given and family components.'),
164      '#default_value' => !empty($settings['allow_family_or_given']),
165      '#table_group' => 'components_extra',
166    ];
167
168    $sort_options = is_array($settings['sort_options']) ? $settings['sort_options'] : [
169      'title' => TRUE,
170      'generational' => FALSE,
171    ];
172    $element['sort_options'] = [
173      '#type' => 'checkboxes',
174      '#title' => $this->t('Sort options'),
175      '#default_value' => array_keys(array_filter($sort_options)),
176      '#description' => $this->t("This enables sorting on the options after the vocabulary terms are added and duplicate values are removed."),
177      '#options' => $metadata
178        ? $metadata->getTranslations([
179          'title' => '',
180          'generational' => '',
181        ])
182        : [],
183    ];
184
185    return $element;
186  }
187
188  /**
189   * Builds per-component field settings elements.
190   *
191   * @param array $settings
192   *   The field settings.
193   * @param array $components
194   *   Translated component labels.
195   * @param array $field_options
196   *   Field type radio options.
197   * @param array $source_options
198   *   Autocomplete source checkbox options.
199   *
200   * @return array
201   *   Form elements keyed by field_type, max_length, autocomplete_source,
202   *   and autocomplete_separator.
203   */
204  private function buildNameFieldPerComponentElements(array $settings, array $components, array $field_options, array $source_options): array {
205    $element = [
206      'field_type' => [
207        '#title' => $this->t('Field type'),
208        '#description' => $this->t('The Field type controls how the field is rendered. Autocomplete is a text field with autocomplete, and the behavior of this is controlled by the field settings.'),
209      ],
210      'max_length' => [
211        '#title' => $this->t('Maximum length'),
212        '#description' => $this->t('The maximum length of the field in characters. This must be between 1 and 255.'),
213      ],
214      'autocomplete_source' => [
215        '#title' => $this->t('Autocomplete sources'),
216        '#description' => $this->t('At least one value must be selected before you can enable the autocomplete option on the input textfields. %field_data suggests values from existing entries the user is allowed to view within the same field storage.', [
217          '%field_data' => $this->t('Field data'),
218        ]),
219      ],
220      'autocomplete_separator' => [
221        '#title' => $this->t('Autocomplete separator'),
222        '#description' => $this->t('This allows you to override the default handling that the autocomplete uses to handle separations between components. If empty, this defaults to a single space.'),
223      ],
224    ];
225
226    foreach ($components as $key => $title) {
227      $element['max_length'][$key] = [
228        '#type' => 'number',
229        '#min' => 1,
230        '#max' => 255,
231        '#title' => $this->t('Maximum length for @title', ['@title' => $title]),
232        '#title_display' => 'invisible',
233        '#default_value' => $settings['max_length'][$key],
234        '#required' => TRUE,
235        '#size' => 5,
236      ];
237      $element['autocomplete_source'][$key] = [
238        '#type' => 'checkboxes',
239        '#title' => $this->t('Autocomplete options'),
240        '#title_display' => 'invisible',
241        '#default_value' => $settings['autocomplete_source'][$key],
242        '#options' => $this->filterAutocompleteSourceOptions($key, $source_options),
243      ];
244      $element['autocomplete_separator'][$key] = [
245        '#type' => 'textfield',
246        '#title' => $this->t('Autocomplete separator for @title', ['@title' => $title]),
247        '#title_display' => 'invisible',
248        '#default_value' => $settings['autocomplete_separator'][$key],
249        '#size' => 10,
250      ];
251      $element['field_type'][$key] = [
252        '#type' => 'radios',
253        '#title' => $this->t('@title field type', ['@title' => $components['title']]),
254        '#title_display' => 'invisible',
255        '#default_value' => $settings['field_type'][$key],
256        '#required' => TRUE,
257        '#options' => $this->filterFieldTypeOptions($key, $field_options),
258      ];
259    }
260
261    return $element;
262  }
263
264  /**
265   * Filters autocomplete source options for a component.
266   *
267   * @param string $key
268   *   The component key.
269   * @param array $source_options
270   *   All autocomplete source options.
271   *
272   * @return array
273   *   Options applicable to the component.
274   */
275  private function filterAutocompleteSourceOptions(string $key, array $source_options): array {
276    $options = $source_options;
277    if ($key != 'title') {
278      unset($options['title']);
279    }
280    if ($key != 'generational') {
281      unset($options['generational']);
282    }
283    return $options;
284  }
285
286  /**
287   * Filters field type options for a component.
288   *
289   * @param string $key
290   *   The component key.
291   * @param array $field_options
292   *   All field type options.
293   *
294   * @return array
295   *   Options applicable to the component.
296   */
297  private function filterFieldTypeOptions(string $key, array $field_options): array {
298    $options = $field_options;
299    $is_title_or_generational = ($key == 'title' || $key == 'generational');
300    if (!$is_title_or_generational) {
301      unset($options['select']);
302    }
303    return $options;
304  }
305
306  /**
307   * Builds autocomplete match mode form elements.
308   *
309   * @param array $settings
310   *   The field settings.
311   * @param array $components
312   *   Translated component labels.
313   *
314   * @return array
315   *   Autocomplete match mode elements.
316   */
317  private function buildNameFieldAutocompleteMatchElements(array $settings, array $components): array {
318    $element = [];
319    $element['autocomplete_match'] = [
320      '#type' => 'radios',
321      '#title' => $this->t('Default autocomplete match mode'),
322      '#description' => $this->t('Controls how typed text is matched against suggestions. %starts_with is recommended for performance; %contains is more flexible but can be slow on large datasets.', [
323        '%starts_with' => $this->t('Starts with'),
324        '%contains' => $this->t('Contains'),
325      ]),
326      '#options' => [
327        'starts_with' => $this->t('Starts with (recommended)'),
328        'contains' => $this->t('Contains'),
329      ],
330      '#default_value' => $settings['autocomplete_match'] ?? 'starts_with',
331      '#table_group' => 'none',
332    ];
333    $element['autocomplete_match_overrides'] = [
334      '#type' => 'details',
335      '#title' => $this->t('Per-component match mode overrides'),
336      '#description' => $this->t('Optional. Override the default match mode for individual components.'),
337      '#open' => FALSE,
338      '#tree' => TRUE,
339      '#table_group' => 'none',
340    ];
341    foreach ($components as $key => $title) {
342      $element['autocomplete_match_overrides'][$key] = [
343        '#type' => 'select',
344        '#title' => $title,
345        '#options' => [
346          '' => $this->t('Use default'),
347          'starts_with' => $this->t('Starts with'),
348          'contains' => $this->t('Contains'),
349        ],
350        '#default_value' => $settings['autocomplete_match_overrides'][$key] ?? '',
351      ];
352    }
353    return $element;
354  }
355
356  /**
357   * Builds title and generational options textarea elements.
358   *
359   * @param array $settings
360   *   The field settings.
361   * @param array $components
362   *   Translated component labels.
363   *
364   * @return array
365   *   Options textarea form elements.
366   */
367  private function buildNameFieldOptionsTextareaElements(array $settings, array $components): array {
368    // @todo Grouping & grouping sort
369    // @todo Allow reverse free tagging back into the vocabulary.
370    $element = [];
371    $title_options = implode("\n", array_filter($settings['title_options']));
372    $element['title_options'] = [
373      '#type' => 'textarea',
374      '#title' => $this->t('@title options', ['@title' => $components['title']]),
375      '#default_value' => $title_options,
376      '#required' => TRUE,
377      '#description' => $this->t("Enter one @title per line. Prefix a line using '--' to specify a blank value text. For example: '--Please select a @title'.", [
378        '@title' => $components['title'],
379      ]),
380      '#element_validate' => [[get_class($this), 'validateTitleOptions']],
381      '#table_group' => 'none',
382    ];
383    $generational_options = implode("\n", array_filter($settings['generational_options']));
384    $element['generational_options'] = [
385      '#type' => 'textarea',
386      '#title' => $this->t('@generational options', ['@generational' => $components['generational']]),
387      '#default_value' => $generational_options,
388      '#required' => TRUE,
389      '#description' => $this->t("Enter one @generational suffix option per line. Prefix a line using '--' to specify a blank value text. For example: '----'.", [
390        '@generational' => $components['generational'],
391      ]),
392      '#element_validate' => [[get_class($this), 'validateGenerationalOptions']],
393      '#table_group' => 'none',
394    ];
395
396    $module_handler = \Drupal::moduleHandler();
397    if ($module_handler->moduleExists('taxonomy')) {
398      // @todo Make the labels more generic.
399      $element['title_options']['#description'] .= ' ' . $this->t("%label_plural may be also imported from one or more vocabularies using the tag '[vocabulary:xxx]', where xxx is the vocabulary machine-name or id. Terms that exceed the maximum length of the %label are not added to the options list.", [
400        '%label_plural' => $this->t('Titles'),
401        '%label' => $this->t('Title'),
402      ]);
403      $element['generational_options']['#description'] .= ' ' . $this->t("%label_plural may be also imported from one or more vocabularies using the tag '[vocabulary:xxx]', where xxx is the vocabulary machine-name or id. Terms that exceed the maximum length of the %label are not added to the options list.", [
404        '%label_plural' => $this->t('Generational suffixes'),
405        '%label' => $this->t('Generational suffix'),
406      ]);
407    }
408
409    if ($module_handler->moduleExists('help')) {
410      $element['title_options']['#description'] .= ' ' . $this->t('See the help topic for a comprehensive list of @help_topic.', [
411        '@help_topic' => Link::fromTextAndUrl(t('titles'), Url::fromUri('internal:/admin/help/topic/name.titles'))->toString(),
412      ]);
413    }
414
415    return $element;
416  }
417
418  /**
419   * Builds the component layout form element.
420   *
421   * @return array
422   *   The component layout form element.
423   */
424  private function buildNameFieldLayoutElement(): array {
425    $items = [
426      $this->t('The order for Asian names is Family Middle Given Title Credentials'),
427      $this->t('The order for Eastern names is Title Family Given Middle Credentials'),
428      $this->t('The order for German names is Title Credentials Given Middle Surname'),
429      $this->t('The order for Western names is Title Given Middle Surname Credentials'),
430    ];
431    $item_list = [
432      '#theme' => 'item_list',
433      '#items' => $items,
434    ];
435    $layout_description = $this->t('<p>This controls the order of the widgets that are displayed in the form.</p>')
436      . \Drupal::service('renderer')->render($item_list)
437      . $this->t('<p>Note that when you select the Asian and German name formats, the Generational field is hidden and defaults to an empty string.</p>');
438
439    return [
440      'component_layout' => [
441        '#type' => 'radios',
442        '#title' => $this->t('Language layout'),
443        '#default_value' => $this->getSetting('component_layout'),
444        '#options' => [
445          'default' => $this->t('Western names'),
446          'asian' => $this->t('Asian names'),
447          'eastern' => $this->t('Eastern names'),
448          'german' => $this->t('German names'),
449        ],
450        '#description' => $layout_description,
451        '#table_group' => 'above',
452        '#required' => TRUE,
453        '#weight' => -49,
454      ],
455    ];
456  }
457
458  /**
459   * Returns a form for the default settings defined above.
460   *
461   * The following keys are closely tied to the pre-render function to theme
462   * the settings into a nicer table.
463   * - #indent_row: Adds an empty TD cell and adds an 'elements' child that
464   *   contains the children (if given).
465   * - #table_group: Used to either position within the table by the element
466   *   key, or set to 'none', to append it below the table.
467   *
468   * Any element within the table should have component keyed children.
469   *
470   * Other elements are rendered directly.
471   *
472   * @param array $settings
473   *   The settings.
474   *
475   * @return array
476   *   The form definition for the field settings.
477   */
478  protected function getDefaultNameFieldSettingsForm(array $settings) {
479    $metadata = \Drupal::getContainer()
480      ->get('name.component_metadata', ContainerInterface::NULL_ON_INVALID_REFERENCE);
481    $components = $metadata ? $metadata->getTranslations() : [];
482    $field_options = [
483      'select' => $this->t('Drop-down'),
484      'text' => $this->t('Text field'),
485      'autocomplete' => $this->t('Autocomplete'),
486    ];
487    // @todo Refactor out for alternative sources.
488    $source_options = [
489      'title' => $this->t('Title options'),
490      'generational' => $this->t('Generational options'),
491      'data' => $this->t('Field data'),
492    ];
493
494    return array_merge(
495      $this->buildNameFieldComponentCheckboxElements($settings, $components, $metadata),
496      $this->buildNameFieldPerComponentElements($settings, $components, $field_options, $source_options),
497      $this->buildNameFieldAutocompleteMatchElements($settings, $components),
498      $this->buildNameFieldOptionsTextareaElements($settings, $components),
499      $this->buildNameFieldLayoutElement(),
500    );
501  }
502
503  /**
504   * Helper function to validate minimum components.
505   *
506   * @param array $element
507   *   Element being validated.
508   * @param \Drupal\Core\Form\FormStateInterface $form_state
509   *   The form state.
510   */
511  public static function validateMinimumComponents(array $element, FormStateInterface $form_state) {
512    $minimum_components = $form_state->getValue(['settings', 'minimum_components']);
513    $diff = array_intersect(array_keys(array_filter($minimum_components)), ['given', 'family']);
514    if (count($diff) == 0) {
515      $service = \Drupal::getContainer()
516        ->get('name.component_metadata', ContainerInterface::NULL_ON_INVALID_REFERENCE);
517      $translations = $service ? $service->getTranslations() : [];
518      $components = array_intersect_key($translations, array_flip(['given', 'family']));
519      $form_state->setError($element, t('%label must have one of the following components: %components', [
520        '%label' => t('Minimum components'),
521        '%components' => implode(', ', $components),
522      ]));
523    }
524
525    $components = $form_state->getValue(['settings', 'components']);
526    $minimum_components = $form_state->getValue(['settings', 'minimum_components']);
527    $diff = array_diff_key(array_filter($minimum_components), array_filter($components));
528    if (count($diff)) {
529      $service = \Drupal::getContainer()
530        ->get('name.component_metadata', ContainerInterface::NULL_ON_INVALID_REFERENCE);
531      $translations = $service ? $service->getTranslations() : [];
532      $components = array_intersect_key($translations, $diff);
533      $form_state->setError($element, t('%components can not be selected for %label when they are not selected for %label2.', [
534        '%label' => t('Minimum components'),
535        '%label2' => t('Components'),
536        '%components' => implode(', ', $components),
537      ]));
538    }
539  }
540
541  /**
542   * Helper function to validate minimum components.
543   *
544   * @param array $element
545   *   Element being validated.
546   * @param \Drupal\Core\Form\FormStateInterface $form_state
547   *   The form state.
548   */
549  public static function validateTitleOptions($element, FormStateInterface $form_state) {
550    $values = static::extractAllowedValues($element['#value']);
551    $max_length = $form_state->getValue(['settings', 'max_length', 'title']);
552    static::validateOptions($element, $form_state, $values, $max_length);
553  }
554
555  /**
556   * Helper function to validate minimum components.
557   *
558   * @param array $element
559   *   Element being validated.
560   * @param \Drupal\Core\Form\FormStateInterface $form_state
561   *   The form state.
562   */
563  public static function validateGenerationalOptions($element, FormStateInterface $form_state) {
564    $values = static::extractAllowedValues($element['#value']);
565    $max_length_keys = ['settings', 'max_length', 'generational'];
566    $max_length = $form_state->getValue($max_length_keys);
567    static::validateOptions($element, $form_state, $values, $max_length);
568  }
569
570}