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