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