Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 123
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
LayersPanel
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 7
600
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
 keyboardShortcuts
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
 build
0.00% covered (danger)
0.00%
0 / 7
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
 buildSingleComponent
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 buildSingleBlock
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 addThirdPartySettingsSummary
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 addComponentSettingsSummary
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\StringTranslation\TranslatableMarkup;
8use Drupal\display_builder\Attribute\Island;
9use Drupal\display_builder\InstanceInterface;
10use Drupal\display_builder\Island\IslandPluginManagerInterface;
11use Drupal\display_builder\Island\IslandType;
12use Drupal\display_builder\SourceWithSlotsInterface;
13use Drupal\display_builder\ThirdPartySettingsInterface;
14use Symfony\Component\DependencyInjection\ContainerInterface;
15
16/**
17 * Layers island plugin implementation.
18 */
19#[Island(
20  id: 'layers',
21  label: new TranslatableMarkup('Layers'),
22  description: new TranslatableMarkup('Manage hierarchical layer view of elements without preview.'),
23  type: IslandType::View,
24  default_region: 'main',
25  icon: 'layers',
26)]
27class LayersPanel extends BuilderPanel {
28
29  /**
30   * Island plugins manager.
31   */
32  protected IslandPluginManagerInterface $islandManager;
33
34  /**
35   * {@inheritdoc}
36   */
37  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
38    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
39    $instance->islandManager = $container->get('plugin.manager.db_island');
40
41    return $instance;
42  }
43
44  /**
45   * {@inheritdoc}
46   */
47  public static function keyboardShortcuts(): array {
48    return [
49      'key' => 'y',
50      'help' => t('Show the layer'),
51    ];
52  }
53
54  /**
55   * {@inheritdoc}
56   */
57  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
58    $build = parent::build($builder, $data, $options);
59
60    if (empty($build['#slots']['content'] ?? [])) {
61      // Load en empty component to have any assets with it.
62      $build['#slots']['content'] = [
63        '#type' => 'component',
64        '#component' => 'display_builder:layer',
65      ];
66    }
67
68    return $build;
69  }
70
71  /**
72   * {@inheritdoc}
73   */
74  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
75    $info = $this->resolveComponentInfo($source, $data, $instance_id);
76
77    if ($info === NULL) {
78      return NULL;
79    }
80
81    ['label' => $label, 'instance_id' => $instance_id] = $info;
82
83    $slots = [];
84
85    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
86      $dropzone = [
87        '#type' => 'component',
88        '#component' => 'display_builder:dropzone',
89        '#props' => [
90          'title' => $definition['title'],
91          'variant' => 'highlighted',
92        ],
93        '#attributes' => [
94          // Required for JavaScript @see components/dropzone/dropzone.js.
95          'data-db-id' => $builder_id,
96          // Slot is needed for contextual menu paste.
97          // @see assets/js/contextual_menu.js
98          'data-slot-id' => $slot_id,
99          'data-slot-title' => $definition['title'],
100          'data-node-title' => $label,
101          'data-instance-id' => $instance_id . '_' . $slot_id,
102        ],
103      ];
104
105      if ($sources = $source->getSlotValue($slot_id)) {
106        $dropzone['#slots']['content'] = $this->digFromSlot($builder_id, $sources);
107      }
108      $dropzone = $this->htmxEvents->onSlotDrop($dropzone, $builder_id, $this->getPluginID(), $instance_id, $slot_id);
109      $slots[] = [
110        [
111          '#plain_text' => $definition['title'],
112        ],
113        $dropzone,
114      ];
115    }
116
117    $build = [
118      '#type' => 'component',
119      '#component' => 'display_builder:layer',
120      '#slots' => [
121        'title' => $label,
122        'children' => $slots,
123      ],
124      // Required for the context menu label.
125      // @see assets/js/contextual_menu.js
126      '#attributes' => [
127        'data-node-title' => $label,
128        'data-instance-id' => $instance_id,
129      ],
130    ];
131    $build = $this->addThirdPartySettingsSummary($data, $build);
132    $build = $this->addComponentSettingsSummary($source, $build);
133
134    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
135  }
136
137  /**
138   * {@inheritdoc}
139   */
140  protected function buildSingleBlock(string $builder_id, string $instance_id, array $data, int $index = 0): array {
141    $label = $this->slotSourceProxy->getLabelWithSummary($data, $this->configuration['contexts'] ?? []);
142
143    if (isset($data['source_id']) && $data['source_id'] === 'entity_field') {
144      $label['summary'] = (string) $this->t('Field: @label', ['@label' => $label['label']]);
145    }
146
147    $build = [
148      '#type' => 'component',
149      '#component' => 'display_builder:layer',
150      '#slots' => [
151        'title' => $label['summary'],
152      ],
153    ];
154
155    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
156
157    if (!$instance_id) {
158      $this->logger->error('[LayersPanel::buildSingleBlock] missing instance ID. <pre>' . \print_r($data, TRUE) . '</pre>');
159
160      return $build;
161    }
162
163    $build = $this->addThirdPartySettingsSummary($data, $build);
164
165    // This label is used for contextual menu.
166    // @see assets/js/contextual_menu.js
167    $build['#attributes']['data-node-title'] = $label['summary'];
168    $build['#attributes']['data-slot-position'] = $index;
169    $build['#attributes']['data-instance-id'] = $instance_id;
170
171    // Add data-node-type for easier identification of block types in JS, CSS or
172    // tests.
173    if (isset($data['source_id'])) {
174      $build['#attributes']['data-node-type'] = $data['source_id'];
175    }
176
177    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $label['summary'], $index);
178  }
179
180  /**
181   * Add third party settings summary to layer's info slot.
182   *
183   * @param array $data
184   *   The node data.
185   * @param array $build
186   *   The layer component renderable array.
187   *
188   * @return array
189   *   The layer component renderable array.
190   */
191  private function addThirdPartySettingsSummary(array $data, array $build): array {
192    if (!isset($data['third_party_settings'])) {
193      return $build;
194    }
195
196    foreach ($data['third_party_settings'] as $provider => $settings) {
197      // In Display Builder, third_party_settings providers can be:
198      // - an island plugin ID (our 'normal' way)
199      // - a Drupal module name (the Drupal way, found in displays imported and
200      // converted, not leveraged by us for now but we may do it later).
201      // So, let's check the plugin ID exists before running logic.
202      if (!$this->islandManager->hasDefinition($provider)) {
203        continue;
204      }
205      $island = $this->islandManager->createInstance($provider, $settings);
206
207      if ($island instanceof ThirdPartySettingsInterface && $summary = $island->getSummary()) {
208        $build['#slots']['info'] = \array_merge($build['#slots']['info'] ?? [], $summary);
209      }
210    }
211
212    return $build;
213  }
214
215  /**
216   * Add config settings summary to layer's info slot.
217   *
218   * @param \Drupal\display_builder\SourceWithSlotsInterface $source
219   *   The source plugin.
220   * @param array $build
221   *   The layer component renderable array.
222   *
223   * @return array
224   *   The layer component renderable array.
225   */
226  private function addComponentSettingsSummary(SourceWithSlotsInterface $source, array $build): array {
227    $items = [];
228
229    foreach ($source->settingsSummary() as $item) {
230      if ($item !== NULL) {
231        $items[] = [
232          '#type' => 'html_tag',
233          '#tag' => 'li',
234          '#value' => $item,
235        ];
236      }
237    }
238
239    if (empty($items)) {
240      return $build;
241    }
242
243    $summary = [
244      [
245        '#type' => 'html_tag',
246        '#tag' => 'em',
247        '#value' => new TranslatableMarkup('Config'),
248      ],
249      [
250        '#type' => 'html_tag',
251        '#tag' => 'ul',
252        '#attributes' => [
253          'class' => ['summary'],
254        ],
255        0 => $items,
256      ],
257    ];
258
259    $build['#slots']['info'] = \array_merge($build['#slots']['info'] ?? [], $summary);
260
261    return $build;
262  }
263
264}