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