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