Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
55.56% covered (warning)
55.56%
65 / 117
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PrimaryEntityReferenceOptionsButtonsWidget
55.56% covered (warning)
55.56%
65 / 117
80.00% covered (warning)
80.00%
4 / 5
149.78
0.00% covered (danger)
0.00%
0 / 1
 defaultSettings
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 settingsForm
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
1
 settingsSummary
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 formElement
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
156
 massageFormValues
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
20
1<?php
2
3namespace Drupal\primary_entity_reference\Plugin\Field\FieldWidget;
4
5use Drupal\Core\Field\Attribute\FieldWidget;
6use Drupal\Core\Field\FieldItemListInterface;
7use Drupal\Core\Form\FormStateInterface;
8use Drupal\Core\StringTranslation\TranslatableMarkup;
9use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsButtonsWidget;
10
11/**
12 * Plugin implementation of the 'options_buttons' widget.
13 */
14#[FieldWidget(
15  id: 'primary_entity_reference_options_buttons',
16  label: new TranslatableMarkup('Check boxes/radio buttons'),
17  field_types: [
18    'primary_entity_reference',
19  ],
20  multiple_values: TRUE,
21)]
22class PrimaryEntityReferenceOptionsButtonsWidget extends OptionsButtonsWidget {
23
24  /**
25   * {@inheritdoc}
26   */
27  public static function defaultSettings() {
28    return [
29      'primary_label'     => 'Primary',
30      'show_primary_only' => FALSE,
31    ] + parent::defaultSettings();
32  }
33
34  /**
35   * {@inheritdoc}
36   */
37  public function settingsForm(array $form, FormStateInterface $form_state) {
38    $element = parent::settingsForm($form, $form_state);
39
40    $element['primary_label'] = [
41      '#type'          => 'textfield',
42      '#title'         => $this->t('Primary label'),
43      '#description'   => $this->t('The label to display for the primary selection.'),
44      '#default_value' => $this->getSetting('primary_label'),
45    ];
46
47    $element['show_primary_only'] = [
48      '#type'          => 'checkbox',
49      '#title'         => $this->t('Show primary only'),
50      '#description'   => $this->t('Only display the primary item in the form. All non-primary items will be preserved but not shown.'),
51      '#default_value' => $this->getSetting('show_primary_only'),
52    ];
53
54    return $element;
55  }
56
57  /**
58   * {@inheritdoc}
59   */
60  public function settingsSummary() {
61    $summary = parent::settingsSummary();
62
63    $primary_label = $this->getSetting('primary_label');
64    $summary[] = $this->t('Primary label: @label', ['@label' => $primary_label]);
65
66    if ($this->getSetting('show_primary_only')) {
67      $summary[] = $this->t('Only showing primary item in form.');
68    }
69
70    return $summary;
71  }
72
73  /**
74   * {@inheritdoc}
75   */
76  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
77    $el = parent::formElement($items, $delta, $element, $form, $form_state);
78    // Alter the checkboxes to include a radio per option.
79    $values = $items->getValue();
80    $options = $this->getOptions($items->getEntity());
81    $default_values = array_column($values, 'target_id');
82
83    $primary = $default_values[0] ?? NULL;
84
85    $field_name = $this->fieldDefinition->getName();
86    $elements['#type'] = 'container';
87    $elements['#tree'] = TRUE;
88    $elements['#attached']['library'][] = 'primary_entity_reference/primary_selection';
89
90    // Handle show_primary_only setting.
91    $show_primary_only = $this->getSetting('show_primary_only');
92    if ($show_primary_only && $primary !== NULL) {
93      // Only show the primary option.
94      if (isset($options[$primary])) {
95        $options = [$primary => $options[$primary]];
96      }
97    }
98
99    // Add headers if not showing primary only.
100    if (!$show_primary_only) {
101      $primary_label = $this->getSetting('primary_label');
102      $field_label = $this->fieldDefinition->getLabel();
103
104      // If primary label is empty, show only field label (left-aligned).
105      if (empty($primary_label)) {
106        $elements['#prefix'] = '<div class="primary-entity-reference-single-header">';
107        $elements['#prefix'] .= $field_label;
108        $elements['#prefix'] .= '</div>';
109      }
110      else {
111        // Show two-column header with Primary and field label.
112        $elements['#prefix'] = '<div class="primary-entity-reference-options-header">';
113        $elements['#prefix'] .= '<span class="primary-column-header">' . $this->t('@label', ['@label' => $primary_label]) . '</span>';
114        $elements['#prefix'] .= '<span class="options-column-header">' . $field_label . '</span>';
115        $elements['#prefix'] .= '</div>';
116      }
117    }
118
119    // Always show primary column (radio buttons) when not in primary-only mode.
120    $show_primary_column = !$show_primary_only;
121    $primary_label       = $this->getSetting('primary_label');
122    $field_label         = $this->fieldDefinition->getLabel();
123    $radio_title         = !empty($primary_label) ? $primary_label : $field_label;
124
125    foreach ($options as $key => $label) {
126      $elements[$key] = [
127        '#type'       => 'container',
128        '#attributes' => ['class' => [$show_primary_column ? 'checkbox-radio-pair' : 'checkbox-only']],
129      ];
130
131      // Show primary radio when not in primary-only mode.
132      if ($show_primary_column) {
133        $elements[$key]['primary'] = [
134          '#type'           => 'radio',
135          '#title'          => $this->t('@label', ['@label' => $radio_title]),
136          '#title_display'  => 'invisible',
137          '#return_value'   => $key,
138          '#parents'        => [$field_name, 0, 'primary'],
139          '#default_value'  => ($key == $primary) ? $key : 0,
140          '#attributes'     => [
141            'class' => ['primary-radio'],
142          ],
143        ];
144      }
145
146      $elements[$key]['target_id'] = [
147        '#type'          => 'checkbox',
148        '#title'         => $label,
149        "#key_column"    => "target_id",
150        '#return_value'  => $key,
151        '#default_value' => in_array($key, $default_values) ? $key : 0,
152      ];
153    }
154
155    return $elements;
156  }
157
158  /**
159   * {@inheritdoc}
160   */
161  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
162    // If show_primary_only is enabled, preserve non-primary items.
163    $show_primary_only = $this->getSetting('show_primary_only');
164    $original_items = [];
165    if ($show_primary_only) {
166      // Get the original entity from form state.
167      $form_object = $form_state->getFormObject();
168      if (method_exists($form_object, 'getEntity')) {
169        $entity = $form_object->getEntity();
170        $field_name = $this->fieldDefinition->getName();
171        if ($entity && $entity->hasField($field_name)) {
172          $original_items = $entity->get($field_name)->getValue();
173        }
174      }
175    }
176
177    $values = parent::massageFormValues($values, $form, $form_state);
178    $primary = 0;
179    foreach ($values as $delta => $value) {
180      if (isset($value['primary']) && $value['primary']) {
181        $primary = (int) $value['primary'];
182      }
183    }
184
185    $vals = [];
186    $i = 0;
187    foreach ($values as $value) {
188      if (isset($value['primary']) && $value['primary']) {
189        continue;
190      }
191
192      // Skip unchecked options (empty or zero target_id).
193      if (empty($value['target_id'])) {
194        continue;
195      }
196
197      $vals[$i]['target_id'] = $value['target_id'];
198      $vals[$i]['primary'] = 0;
199      if ($value['target_id'] == $primary) {
200        $vals[$i]['primary'] = 1;
201      }
202
203      $i += 1;
204    }
205
206    // If show_primary_only is enabled, merge back non-primary items.
207    if ($show_primary_only && !empty($original_items)) {
208      // Filter out the original primary item.
209      $non_primary_items = array_filter($original_items, fn($item) => empty($item['primary']));
210
211      // Merge processed values with non-primary items.
212      $vals = array_merge($vals, $non_primary_items);
213    }
214
215    // Validation: Ensure primary is selected when multiple items are chosen.
216    $selected_count = count($vals);
217
218    if ($selected_count > 1) {
219      // Check if any item is marked as primary.
220      $has_primary = FALSE;
221      foreach ($vals as $val) {
222        if (!empty($val['primary'])) {
223          $has_primary = TRUE;
224          break;
225        }
226      }
227
228      if (!$has_primary) {
229        $form_state->setError(
230          $form,
231          $this->t('You must select a primary entity reference when multiple items are selected.')
232        );
233      }
234    }
235    elseif ($selected_count === 1) {
236      // Auto-set single item as primary.
237      $vals[0]['primary'] = 1;
238    }
239
240    return $vals;
241  }
242
243}