Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContextualFormPanel
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 12
1056
0.00% covered (danger)
0.00%
0 / 1
 buildForm
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 build
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 onAttachToRoot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onAttachToSlot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onUpdate
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onDelete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isApplicable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 alterFormValues
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
72
 isEmpty
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isMultipleItemsSlotSource
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 removeItemSelector
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\Form\FormStateInterface;
8use Drupal\Core\Render\Element;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\display_builder\Attribute\Island;
11use Drupal\display_builder\InstanceInterface;
12use Drupal\display_builder\Island\IslandPluginBase;
13use Drupal\display_builder\Island\IslandType;
14use Drupal\display_builder\Island\IslandWithFormInterface;
15use Drupal\display_builder\Island\IslandWithFormTrait;
16use Drupal\display_builder\SourceWithSlotsInterface;
17
18/**
19 * Instance form island plugin implementation.
20 */
21#[Island(
22  id: 'contextual_form',
23  enabled_by_default: TRUE,
24  label: new TranslatableMarkup('Config'),
25  description: new TranslatableMarkup('Configure the active component or block.'),
26  type: IslandType::Contextual,
27)]
28class ContextualFormPanel extends IslandPluginBase implements IslandWithFormInterface {
29
30  use IslandWithFormTrait;
31
32  /**
33   * {@inheritdoc}
34   */
35  public function buildForm(array &$form, FormStateInterface $form_state): void {
36    try {
37      $contexts = $form_state->getBuildInfo()['args'][1] ?? [];
38
39      $this->alterFormValues($form_state);
40      $source = $this->sourceManager->getSource($this->data['node_id'], [], $this->data, $contexts);
41
42      if ($source instanceof SourceWithSlotsInterface) {
43        $form = $source->settingsFormPropsOnly([], $form_state);
44      }
45      else {
46        $form = $source ? $source->settingsForm([], $form_state) : [];
47      }
48
49      if ($this->isMultipleItemsSlotSource($this->data['source'])) {
50        $form = $this->removeItemSelector($form);
51      }
52    }
53    catch (\Exception $e) {
54      $this->logger->error('Error building contextual form for node @node_id: @message', [
55        '@node_id' => $this->data['node_id'] ?? 'unknown',
56        '@message' => $e->getMessage(),
57      ]);
58    }
59  }
60
61  /**
62   * {@inheritdoc}
63   */
64  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
65    $build = parent::build($builder, $data, $options);
66
67    if (empty($build)) {
68      return $build;
69    }
70
71    if (self::isEmpty($build)) {
72      return [
73        '#type' => 'html_tag',
74        '#tag' => 'p',
75        '#value' => $this->t('No configuration required.'),
76        '#attributes' => [
77          'class' => ['description'],
78        ],
79      ];
80    }
81
82    $build = [
83      'source' => $build,
84    ];
85
86    $build['update'] = [
87      '#type' => 'button',
88      '#value' => 'Update',
89      '#submit_button' => FALSE,
90      '#attributes' => [
91        'type' => 'button',
92        'data-wysiwyg-fix' => TRUE,
93      ],
94    ];
95
96    $build = $this->htmxEvents->onInstanceFormChange($build, $this->builderId, $this->getPluginId(), $this->data['node_id']);
97
98    return $this->htmxEvents->onInstanceUpdateButtonClick($build, $this->builderId, $this->getPluginId(), $this->data['node_id']);
99  }
100
101  /**
102   * {@inheritdoc}
103   */
104  public function onAttachToRoot(InstanceInterface $instance, string $node_id): array {
105    return $this->reloadWithNodeData($instance, $node_id);
106  }
107
108  /**
109   * {@inheritdoc}
110   */
111  public function onAttachToSlot(InstanceInterface $instance, string $node_id, string $parent_id): array {
112    return $this->reloadWithNodeData($instance, $node_id);
113  }
114
115  /**
116   * {@inheritdoc}
117   */
118  public function onActive(InstanceInterface $instance, array $data): array {
119    return $this->reloadWithLocalData($instance, $data);
120  }
121
122  /**
123   * {@inheritdoc}
124   */
125  public function onUpdate(InstanceInterface $instance, string $node_id): array {
126    // Reload the form itself on update.
127    $data = $instance->getNode($node_id);
128
129    return $this->reloadWithLocalData($instance, $data);
130  }
131
132  /**
133   * {@inheritdoc}
134   */
135  public function onDelete(InstanceInterface $instance, ?string $parent_id): array {
136    return $this->reloadWithLocalData($instance, []);
137  }
138
139  /**
140   * {@inheritdoc}
141   */
142  public function isApplicable(): bool {
143    return isset($this->data['source_id']) && isset($this->data['node_id']);
144  }
145
146  /**
147   * Alter the form values.
148   *
149   * @param \Drupal\Core\Form\FormStateInterface $form_state
150   *   The form state.
151   */
152  protected function alterFormValues(FormStateInterface $form_state): void {
153    // When this is an Ajax CALL, we directly inject the data into the source
154    // settings, but not during the rebuilt.
155    $values = $form_state->getValues();
156
157    if (isset($values['_drupal_ajax']) && $values['_drupal_ajax'] && !$form_state->isRebuilding()) {
158      if ($this->data['source_id'] !== 'component') {
159        $this->data['source'] = $values;
160      }
161    }
162
163    // When rebuilding the form, we need to inject the values into the source
164    // settings.
165    if ($form_state->isRebuilding()) {
166      // Allow to get the posted values through ajax, and give them to the
167      // source plugin through its settings (essential).
168      if (isset($values['source'])) {
169        unset($values['source']);
170      }
171
172      if (!empty($values)) {
173        $this->data['source'] = $values;
174      }
175    }
176  }
177
178  /**
179   * Indicates whether the given form array is empty.
180   *
181   * @param array $form
182   *   The form.
183   *
184   * @return bool
185   *   Whether the given element is empty.
186   */
187  private static function isEmpty(array $form) {
188    $keys = Element::children($form);
189
190    // Quick valid if a component. An empty component is a rare occurrence.
191    if (isset($keys['component'])) {
192      return FALSE;
193    }
194
195    return \array_diff(Element::children($form), [
196      'plugin_id',
197      'form_build_id',
198      'form_token',
199      'form_id',
200      // Exclude some core block with no configuration.
201      // @todo remove when we do not need the update button anymore.
202      'help_block',
203      'local_actions_block',
204      'node_syndicate_block',
205      'system_breadcrumb_block',
206      'system_clear_cache_block',
207      'system_messages_block',
208      'system_powered_by_block',
209    ]) === [];
210  }
211
212  /**
213   * Has the slot source multiple items?
214   *
215   * Some slot sources have 'multiple' items, with a select form element first,
216   * then an item specific form changing with Ajax. They have both a plugin_id
217   * key and a dynamic key with the value of the plugin_id.
218   *
219   * @param array $data
220   *   The slot source data containing:
221   *   - plugin_id: The plugin ID.
222   *
223   * @return bool
224   *   Is multiple or not.
225   */
226  private function isMultipleItemsSlotSource(array $data): bool {
227    if (!isset($data['plugin_id']) || !\is_string($data['plugin_id'])) {
228      return FALSE;
229    }
230
231    if (\count($data) === 1) {
232      // If there is only plugin_id, without any settings, it is OK.
233      return TRUE;
234    }
235    // If there are settings, we need at least the one specific to the item.
236    $plugin_id = (string) $data['plugin_id'];
237
238    if (isset($data[$plugin_id]) && \is_array($data[$plugin_id])) {
239      return TRUE;
240    }
241
242    return FALSE;
243  }
244
245  /**
246   * Remove the item selector from a form.
247   *
248   * For multiple items slot sources, we don't want to show the item selector
249   * since it is already selected in the slot configuration.
250   *
251   * @param array $form
252   *   The form array.
253   *
254   * @return array
255   *   The modified form array.
256   */
257  private function removeItemSelector(array $form): array {
258    $form['plugin_id']['#type'] = 'hidden';
259    unset($form['plugin_id']['#options']);
260
261    return $form;
262  }
263
264}