Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
60.00% covered (warning)
60.00%
51 / 85
69.39% covered (warning)
69.39%
34 / 49
65.12% covered (warning)
65.12%
28 / 43
69.70% covered (warning)
69.70%
23 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
IslandPluginBase
60.00% covered (warning)
60.00%
51 / 85
69.39% covered (warning)
69.39%
34 / 49
65.12% covered (warning)
65.12%
28 / 43
69.70% covered (warning)
69.70%
23 / 33
107.92
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 create
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 build
57.89% covered (warning)
57.89%
11 / 19
87.50% covered (warning)
87.50%
7 / 8
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 afterBuild
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 isApplicable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
5 / 5
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
 label
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBuilderId
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
 setNodeId
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
 keyboardShortcuts
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTypeId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHtmlId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIcon
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onAttachToRoot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onAttachToSlot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onMove
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onActive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onUpdate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onDelete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onHistoryChange
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onRestore
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
 onRevert
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
 onPublish
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onPresetSave
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getConfiguration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setConfiguration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 defaultConfiguration
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configurationSummary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alterRenderable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildContent
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
 reloadWithGlobalData
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 reloadWithLocalData
0.00% covered (danger)
0.00%
0 / 5
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
 reloadWithNodeData
0.00% covered (danger)
0.00%
0 / 5
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
 getArgs
0.00% covered (danger)
0.00%
0 / 6
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\Island;
6
7use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
8use Drupal\Component\Plugin\PluginBase;
9use Drupal\Core\Entity\EntityTypeManagerInterface;
10use Drupal\Core\Form\FormBuilderInterface;
11use Drupal\Core\Form\FormState;
12use Drupal\Core\Form\FormStateInterface;
13use Drupal\Core\StringTranslation\StringTranslationTrait;
14use Drupal\Core\Theme\ComponentPluginManager;
15use Drupal\display_builder\HtmxEvents;
16use Drupal\display_builder\HtmxTrait;
17use Drupal\display_builder\InstanceInterface;
18use Drupal\display_builder\RenderableBuilderTrait;
19use Drupal\ui_patterns\SourcePluginManager;
20use Psr\Log\LoggerInterface;
21use Symfony\Component\DependencyInjection\ContainerInterface;
22
23/**
24 * Base class for island plugins.
25 */
26abstract class IslandPluginBase extends PluginBase implements IslandInterface {
27
28  use HtmxTrait;
29  use RenderableBuilderTrait;
30  use StringTranslationTrait;
31
32  /**
33   * The island data.
34   */
35  protected array $data;
36
37  /**
38   * The builder id.
39   */
40  protected string $builderId;
41
42  /**
43   * The builder instance entity.
44   */
45  protected ?InstanceInterface $builder = NULL;
46
47  /**
48   * The tree node id (when the island is executed in the context of a node).
49   */
50  protected ?string $nodeId = NULL;
51
52  /**
53   * {@inheritdoc}
54   */
55  public function __construct(
56    array $configuration,
57    $plugin_id,
58    $plugin_definition,
59    protected ComponentPluginManager $sdcManager,
60    protected HtmxEvents $htmxEvents,
61    protected EntityTypeManagerInterface $entityTypeManager,
62    protected SourcePluginManager $sourceManager,
63    protected LoggerInterface $logger,
64    protected FormBuilderInterface $formBuilder,
65  ) {
66    parent::__construct($configuration, $plugin_id, $plugin_definition);
67    $this->data = $configuration;
68    $this->setConfiguration($configuration);
69  }
70
71  /**
72   * {@inheritdoc}
73   */
74  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
75    return new static(
76      $configuration,
77      $plugin_id,
78      $plugin_definition,
79      $container->get('plugin.manager.sdc'),
80      $container->get('display_builder.htmx_events'),
81      $container->get('entity_type.manager'),
82      $container->get('plugin.manager.ui_patterns_source'),
83      $container->get('logger.factory')->get('display_builder'),
84      $container->get('form_builder'),
85    );
86  }
87
88  /**
89   * {@inheritdoc}
90   */
91  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
92    $this->builder = $builder;
93
94    $builder_id = (string) $builder->id();
95    $this->builderId = $builder_id;
96    $this->nodeId = $data['node_id'] ?? NULL;
97
98    // First, get specific data for the plugin.
99    if (isset($data['third_party_settings'][$this->getPluginId()])) {
100      $this->data = $data['third_party_settings'][$this->getPluginId()];
101    }
102    // Otherwise, fallback on global data.
103    else {
104      $this->data = $data;
105    }
106
107    if (!$this->isApplicable()) {
108      return [];
109    }
110
111    if (!$this instanceof IslandWithFormInterface) {
112      return $this->buildContent($builder, $data, $options);
113    }
114
115    $contexts = $this->configuration['contexts'] ?? [];
116
117    $form_state = new FormState();
118
119    // We have to force form to not rebuild, otherwise, we are losing data of an
120    // island plugin when another is submitted.
121    // Example: submitting Styles Panel make lose default form values for
122    // Instance Form Panel.
123    $form_state->setRebuild(FALSE);
124    $form_state->setExecuted();
125
126    $form_state->addBuildInfo('args', [$this->getArgs(), $contexts, $options]);
127    $form_state->setTemporaryValue('gathered_contexts', $contexts);
128
129    $build = $this->formBuilder->buildForm($this::getFormClass(), $form_state);
130
131    return $this->afterBuild($build, $form_state);
132  }
133
134  /**
135   * {@inheritdoc}
136   */
137  public function afterBuild(array $element, FormStateInterface $form_state): array {
138    if (!$this->isApplicable()) {
139      return [];
140    }
141
142    $definition = $this->getPluginDefinition();
143    $island_id = $definition instanceof PluginDefinitionInterface ? $definition->id() : ($definition['id'] ?? '');
144
145    return $this->htmxEvents->onThirdPartyFormChange($element, $this->builderId, $this->nodeId, $island_id);
146  }
147
148  /**
149   * {@inheritdoc}
150   */
151  public function isApplicable(): bool {
152    $definition = $this->getPluginDefinition();
153
154    return $this->nodeId !== NULL && \is_array($definition) && !empty($this->data);
155  }
156
157  /**
158   * {@inheritdoc}
159   */
160  public function label(): string {
161    // Cast the label to a string since it is a TranslatableMarkup object.
162    return (string) $this->pluginDefinition['label'];
163  }
164
165  /**
166   * {@inheritdoc}
167   */
168  public function setBuilderId(string $builder_id): void {
169    $this->builderId = $builder_id;
170  }
171
172  /**
173   * {@inheritdoc}
174   */
175  public function setNodeId(?string $node_id): void {
176    $this->nodeId = $node_id;
177  }
178
179  /**
180   * {@inheritdoc}
181   */
182  public static function keyboardShortcuts(): array {
183    return [];
184  }
185
186  /**
187   * {@inheritdoc}
188   */
189  public function getTypeId(): string {
190    return $this->pluginDefinition['type']->value;
191  }
192
193  /**
194   * {@inheritdoc}
195   */
196  public function getHtmlId(string $builder_id): string {
197    return \implode('-', ['island', $builder_id, $this->pluginDefinition['id']]);
198  }
199
200  /**
201   * {@inheritdoc}
202   */
203  public function getIcon(): ?string {
204    return $this->pluginDefinition['icon'] ?? NULL;
205  }
206
207  /**
208   * {@inheritdoc}
209   */
210  public function onAttachToRoot(InstanceInterface $instance, string $node_id): array {
211    return [];
212  }
213
214  /**
215   * {@inheritdoc}
216   */
217  public function onAttachToSlot(InstanceInterface $instance, string $node_id, string $parent_id): array {
218    return [];
219  }
220
221  /**
222   * {@inheritdoc}
223   */
224  public function onMove(InstanceInterface $instance, string $node_id): array {
225    return [];
226  }
227
228  /**
229   * {@inheritdoc}
230   */
231  public function onActive(InstanceInterface $instance, array $data): array {
232    return [];
233  }
234
235  /**
236   * {@inheritdoc}
237   */
238  public function onUpdate(InstanceInterface $instance, string $node_id): array {
239    return [];
240  }
241
242  /**
243   * {@inheritdoc}
244   */
245  public function onDelete(InstanceInterface $instance, ?string $parent_id): array {
246    return [];
247  }
248
249  /**
250   * {@inheritdoc}
251   */
252  public function onHistoryChange(InstanceInterface $instance): array {
253    return [];
254  }
255
256  /**
257   * {@inheritdoc}
258   */
259  public function onRestore(InstanceInterface $instance): array {
260    return [];
261  }
262
263  /**
264   * {@inheritdoc}
265   */
266  public function onRevert(InstanceInterface $instance): array {
267    return [];
268  }
269
270  /**
271   * {@inheritdoc}
272   */
273  public function onPublish(InstanceInterface $instance): array {
274    return [];
275  }
276
277  /**
278   * {@inheritdoc}
279   */
280  public function onPresetSave(InstanceInterface $instance): array {
281    return [];
282  }
283
284  /**
285   * {@inheritdoc}
286   */
287  public function getConfiguration(): array {
288    return \array_merge($this->defaultConfiguration(), $this->configuration);
289  }
290
291  /**
292   * {@inheritdoc}
293   */
294  public function setConfiguration(array $configuration): void {
295    $this->configuration = \array_merge($this->defaultConfiguration(), $configuration);
296  }
297
298  /**
299   * {@inheritdoc}
300   */
301  public function defaultConfiguration(): array {
302    return [];
303  }
304
305  /**
306   * {@inheritdoc}
307   */
308  public function configurationSummary(): array {
309    return [];
310  }
311
312  /**
313   * {@inheritdoc}
314   */
315  public function alterRenderable(InstanceInterface $instance, array $build): array {
316    return $build;
317  }
318
319  /**
320   * Build content for non-form islands.
321   *
322   * Override this method instead of build() when the plugin does not implement
323   * IslandWithFormInterface but still wants to reuse the preamble logic from
324   * build() (setting builder context, checking isApplicable()).
325   *
326   * @param \Drupal\display_builder\InstanceInterface $builder
327   *   Display builder instance.
328   * @param array $data
329   *   The data array.
330   * @param array $options
331   *   Additional options.
332   *
333   * @return array
334   *   A renderable array.
335   */
336  protected function buildContent(InstanceInterface $builder, array $data, array $options): array {
337    return [];
338  }
339
340  /**
341   * Helper method to reload island with global data.
342   *
343   * @param \Drupal\display_builder\InstanceInterface $instance
344   *   The display builder instance.
345   *
346   * @return array
347   *   Returns a render array with out-of-band commands.
348   */
349  protected function reloadWithGlobalData(InstanceInterface $instance): array {
350    return $this->addOutOfBand(
351      $this->build($instance, $instance->getCurrentState()),
352      '#' . $this->getHtmlId((string) $instance->id()),
353      'innerHTML'
354    );
355  }
356
357  /**
358   * Helper method to reload island with provided local data.
359   *
360   * @param \Drupal\display_builder\InstanceInterface $instance
361   *   The display builder instance.
362   * @param array $data
363   *   The local data array to use for building the island.
364   *
365   * @return array
366   *   Returns a render array with out-of-band commands.
367   */
368  protected function reloadWithLocalData(InstanceInterface $instance, array $data): array {
369    return $this->addOutOfBand(
370      $this->build($instance, $data),
371      '#' . $this->getHtmlId((string) $instance->id()),
372      'innerHTML'
373    );
374  }
375
376  /**
377   * Helper method to reload island with node-specific data.
378   *
379   * @param \Drupal\display_builder\InstanceInterface $instance
380   *   The display builder instance.
381   * @param string $node_id
382   *   The tree node ID.
383   *
384   * @return array
385   *   Returns a render array with out-of-band commands.
386   */
387  protected function reloadWithNodeData(InstanceInterface $instance, string $node_id): array {
388    return $this->addOutOfBand(
389      $this->build($instance, $instance->getNode($node_id)),
390      '#' . $this->getHtmlId($node_id),
391      'innerHTML'
392    );
393  }
394
395  /**
396   * Get args passed to plugin.
397   *
398   * @return array
399   *   Array of arguments.
400   */
401  private function getArgs(): array {
402    return [
403      'island_id' => $this->getPluginId(),
404      'builder_id' => $this->builderId,
405      'instance_id' => $this->nodeId,
406      'instance' => $this->data,
407    ];
408  }
409
410}