Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 179
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 158
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ProfileViewBuilder
0.00% covered (danger)
0.00%
0 / 179
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 158
0.00% covered (danger)
0.00%
0 / 11
1260
0.00% covered (danger)
0.00%
0 / 1
 view
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 buildSlots
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 112
0.00% covered (danger)
0.00%
0 / 1
90
 prepareViewIslands
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 buildContextualIslands
0.00% covered (danger)
0.00%
0 / 16
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
 buildPanes
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 buildStartButtons
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildBuilderTabs
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildMenuWrapper
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getIslandsEnableSorted
0.00% covered (danger)
0.00%
0 / 2
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
 entityTypeManager
0.00% covered (danger)
0.00%
0 / 3
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
 islandPluginManager
0.00% covered (danger)
0.00%
0 / 3
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder;
6
7use Drupal\Core\Entity\EntityInterface;
8use Drupal\Core\Entity\EntityTypeManagerInterface;
9use Drupal\Core\Entity\EntityViewBuilder;
10use Drupal\Core\Security\TrustedCallbackInterface;
11
12/**
13 * View builder handler for display builder profiles.
14 */
15class ProfileViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface {
16
17  use RenderableBuilderTrait;
18
19  /**
20   * The entity type manager.
21   */
22  private EntityTypeManagerInterface $entityTypeManager;
23
24  /**
25   * The display builder island plugin manager.
26   */
27  private IslandPluginManagerInterface $islandPluginManager;
28
29  /**
30   * The entity we are building the view for.
31   */
32  private ProfileInterface $entity;
33
34  /**
35   * {@inheritdoc}
36   */
37  public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL): array {
38    // We have 'hacked' the interface by using $view_mode as a way of passing
39    // the Instance entity ID.
40    $builder_id = $view_mode;
41
42    /** @var \Drupal\display_builder\ProfileInterface $entity */
43    $entity = $entity;
44    $this->entity = $entity;
45
46    /** @var \Drupal\display_builder\InstanceInterface $builder */
47    $builder = $this->entityTypeManager()->getStorage('display_builder_instance')->load($builder_id);
48    $contexts = $builder->getContexts() ?? [];
49    $islands_enabled_sorted = $this->getIslandsEnableSorted($contexts);
50    $build = [
51      '#type' => 'component',
52      '#component' => 'display_builder:display_builder',
53      '#props' => [
54        'builder_id' => $builder_id,
55        'hash' => (string) $builder->getCurrent()->hash,
56      ],
57      '#slots' => $this->buildSlots($builder, $islands_enabled_sorted),
58      '#attached' => [
59        'drupalSettings' => [
60          'dbDebug' => $entity->isDebugModeActivated(),
61        ],
62      ],
63      '#cache' => [
64        'tags' => $builder->getCacheTags(),
65      ],
66    ];
67
68    foreach ($islands_enabled_sorted as $islands) {
69      foreach ($islands as $island) {
70        $build = $island->alterRenderable($builder, $build);
71      }
72    }
73
74    return $build;
75  }
76
77  /**
78   * Builds and returns the value of each slot.
79   *
80   * @param \Drupal\display_builder\InstanceInterface $builder
81   *   Display builder instance.
82   * @param array $islands_enabled_sorted
83   *   An array of enabled islands.
84   *
85   * @return array
86   *   An associative array with the value of each slot.
87   */
88  private function buildSlots(InstanceInterface $builder, array $islands_enabled_sorted): array {
89    $builder_data = $builder->getCurrentState();
90
91    $button_islands = $islands_enabled_sorted[IslandType::Button->value] ?? [];
92    $library_islands = $islands_enabled_sorted[IslandType::Library->value] ?? [];
93    $contextual_islands = $islands_enabled_sorted[IslandType::Contextual->value] ?? [];
94    $menu_islands = $islands_enabled_sorted[IslandType::Menu->value] ?? [];
95    $view_islands = $islands_enabled_sorted[IslandType::View->value] ?? [];
96
97    $buttons = [];
98
99    if (!empty($button_islands)) {
100      $buttons = $this->buildPanes($builder, $button_islands, [], [], 'span');
101    }
102
103    if (!empty($menu_islands)) {
104      $menu_islands = $this->buildMenuWrapper($builder, $menu_islands);
105    }
106
107    if (!empty($library_islands)) {
108      $library_islands = [
109        $this->buildBuilderTabs($builder, $library_islands, TRUE),
110        $this->buildPanes($builder, $library_islands, $builder_data),
111      ];
112    }
113
114    $view_islands_data = $this->prepareViewIslands($builder, $view_islands);
115    $view_sidebar = $view_islands_data['view_sidebar'];
116    $view_main = $view_islands_data['view_main'];
117
118    // Library content can be in main or sidebar.
119    // @todo Move the logic to LibrariesPanel::build().
120    // @see https://www.drupal.org/project/display_builder/issues/3542866
121    if (isset($view_sidebar['library']) && !empty($library_islands)) {
122      $view_sidebar['library']['content'] = $library_islands;
123    }
124    elseif (isset($view_main['library']) && !empty($library_islands)) {
125      $view_main['library']['content'] = $library_islands;
126    }
127
128    if (!empty($contextual_islands)) {
129      $contextual_islands = $this->buildContextualIslands($builder, $islands_enabled_sorted);
130    }
131
132    return [
133      'view_sidebar_buttons' => $view_islands_data['view_sidebar_buttons'],
134      'view_sidebar' => $view_sidebar,
135      'view_main_tabs' => $view_islands_data['view_main_tabs'],
136      'view_main' => $view_main,
137      'buttons' => $buttons,
138      'contextual_islands' => $contextual_islands,
139      'menu_islands' => $menu_islands,
140    ];
141  }
142
143  /**
144   * Prepares view islands data.
145   *
146   * @param \Drupal\display_builder\InstanceInterface $builder
147   *   Display builder instance.
148   * @param array $islands
149   *   The sorted, enabled View islands.
150   *
151   * @return array
152   *   The prepared view islands data.
153   */
154  private function prepareViewIslands(InstanceInterface $builder, array $islands): array {
155    $view_islands_sidebar = [];
156    $view_islands_main = [];
157    $view_sidebar_buttons = [];
158    $view_main_tabs = [];
159
160    foreach ($islands as $id => $island) {
161      if ($island->getTypeId() !== IslandType::View->value) {
162        continue;
163      }
164
165      $configuration = $island->getConfiguration();
166
167      if ($configuration['region'] === 'sidebar') {
168        $view_islands_sidebar[$id] = $islands[$id];
169        $view_sidebar_buttons[$id] = $islands[$id];
170      }
171      else {
172        $view_islands_main[$id] = $islands[$id];
173        $view_main_tabs[$id] = $islands[$id];
174      }
175    }
176
177    if (!empty($view_sidebar_buttons)) {
178      $view_sidebar_buttons = $this->buildStartButtons($builder, $view_sidebar_buttons);
179    }
180
181    if (!empty($view_main_tabs)) {
182      $view_main_tabs = $this->buildBuilderTabs($builder, $view_main_tabs, FALSE);
183    }
184
185    $builder_data = $builder->getCurrentState();
186    $view_sidebar = $this->buildPanes($builder, $view_islands_sidebar, $builder_data);
187    // Default hidden.
188    $view_main = $this->buildPanes($builder, $view_islands_main, $builder_data, ['shoelace-tabs__tab--hidden']);
189
190    return [
191      'view_sidebar_buttons' => $view_sidebar_buttons,
192      'view_main_tabs' => $view_main_tabs,
193      'view_sidebar' => $view_sidebar,
194      'view_main' => $view_main,
195    ];
196  }
197
198  /**
199   * Build contextual islands which are tabbed sub islands.
200   *
201   * @param \Drupal\display_builder\InstanceInterface $builder
202   *   Display builder instance.
203   * @param array $islands_enabled_sorted
204   *   The islands enabled sorted.
205   *
206   * @return array
207   *   The contextual islands render array.
208   */
209  private function buildContextualIslands(InstanceInterface $builder, array $islands_enabled_sorted): array {
210    $contextual_islands = $islands_enabled_sorted[IslandType::Contextual->value] ?? [];
211
212    if (empty($contextual_islands)) {
213      return [];
214    }
215
216    $filter = $this->buildInput((string) $builder->id(), '', 'search', 'medium', 'off', $this->t('Filter by name'), TRUE, 'search');
217    // @see assets/js/search.js
218    $filter['#attributes']['class'] = ['db-search-instance'];
219
220    return [
221      '#type' => 'html_tag',
222      '#tag' => 'div',
223      // Used for custom styling in assets/css/form.css.
224      '#attributes' => [
225        'id' => \sprintf('%s-contextual', $builder->id()),
226        'class' => ['db-form'],
227      ],
228      'tabs' => $this->buildBuilderTabs($builder, $contextual_islands),
229      'filter' => $filter,
230      'panes' => $this->buildPanes($builder, $contextual_islands, $builder->getCurrentState()),
231    ];
232  }
233
234  /**
235   * Builds panes.
236   *
237   * @param \Drupal\display_builder\InstanceInterface $builder
238   *   Display builder instance.
239   * @param \Drupal\display_builder\IslandInterface[] $islands
240   *   The islands to build tabs for.
241   * @param array $data
242   *   (Optional) The data to pass to the islands.
243   * @param array $classes
244   *   (Optional) The HTML classes to start with.
245   * @param string $tag
246   *   (Optional) The HTML tag, defaults to 'div'.
247   *
248   * @return array
249   *   The tabs render array.
250   */
251  private function buildPanes(InstanceInterface $builder, array $islands, array $data = [], array $classes = [], string $tag = 'div'): array {
252    $panes = [];
253
254    foreach ($islands as $island_id => $island) {
255      $island_classes = \array_merge($classes, [
256        'db-island',
257        \sprintf('db-island-%s', $island->getTypeId()),
258        \sprintf('db-island-%s', $island->getPluginId()),
259      ]);
260
261      $panes[$island_id] = [
262        '#type' => 'html_tag',
263        '#tag' => $tag,
264        'children' => $island->build($builder, $data),
265        '#attributes' => [
266          // `id` attribute is used by HTMX OOB swap.
267          'id' => $island->getHtmlId((string) $builder->id()),
268          // `sse-swap` attribute is used by HTMX SSE swap.
269          'sse-swap' => $island->getHtmlId((string) $builder->id()),
270          'class' => $island_classes,
271        ],
272      ];
273    }
274
275    return $panes;
276  }
277
278  /**
279   * Build the buttons to hide/show the drawer.
280   *
281   * @param \Drupal\display_builder\InstanceInterface $builder
282   *   Display builder instance.
283   * @param \Drupal\display_builder\IslandInterface[] $islands
284   *   An array of island objects for which buttons will be created.
285   *
286   * @return array
287   *   An array of render arrays for the drawer buttons.
288   */
289  private function buildStartButtons(InstanceInterface $builder, array $islands): array {
290    $build = [];
291
292    foreach ($islands as $island) {
293      $island_id = $island->getPluginId();
294
295      $build[$island_id] = [
296        '#type' => 'component',
297        '#component' => 'display_builder:button',
298        '#props' => [
299          'id' => \sprintf('start-btn-%s-%s', $builder->id(), $island_id),
300          'label' => (string) $island->label(),
301          'icon' => $island->getIcon(),
302          'attributes' => [
303            'data-open-first-drawer' => TRUE,
304            'data-target' => $island_id,
305          ],
306        ],
307      ];
308
309      if ($keyboard = $island::keyboardShortcuts()) {
310        $build[$island_id]['#attributes']['data-keyboard-key'] = \key($keyboard);
311        $build[$island_id]['#attributes']['data-keyboard-help'] = \reset($keyboard);
312      }
313    }
314
315    return $build;
316  }
317
318  /**
319   * Builds tabs.
320   *
321   * @param \Drupal\display_builder\InstanceInterface $builder
322   *   Display builder instance.
323   * @param \Drupal\display_builder\IslandInterface[] $islands
324   *   The islands to build tabs for.
325   * @param bool $contextual
326   *   (Optional) Whether the tabs are contextual.
327   *
328   * @return array
329   *   The tabs render array.
330   */
331  private function buildBuilderTabs(InstanceInterface $builder, array $islands, bool $contextual = FALSE): array {
332    // Global id is based on last island.
333    $id = '';
334    $tabs = [];
335
336    foreach ($islands as $island) {
337      $id = $island_id = $island->getHtmlId((string) $builder->id());
338
339      $attributes = [];
340
341      if ($keyboard = $island::keyboardShortcuts()) {
342        $attributes['data-keyboard-key'] = \key($keyboard);
343        $attributes['data-keyboard-help'] = \reset($keyboard);
344      }
345      $tabs[] = [
346        'title' => $island->label(),
347        'url' => '#' . $island_id,
348        'attributes' => $attributes,
349      ];
350    }
351
352    // Id is needed for storage tabs state, @see component tabs.js file.
353    return $this->buildTabs($id, $tabs, $contextual);
354  }
355
356  /**
357   * Builds menu with islands as entries.
358   *
359   * @param \Drupal\display_builder\InstanceInterface $builder
360   *   Display builder instance.
361   * @param \Drupal\display_builder\IslandInterface[] $islands
362   *   The islands to build tabs for.
363   *
364   * @return array
365   *   The islands render array.
366   *
367   * @see assets/js/contextual_menu.js
368   */
369  private function buildMenuWrapper(InstanceInterface $builder, array $islands): array {
370    $build = [
371      '#type' => 'component',
372      '#component' => 'display_builder:contextual_menu',
373      '#slots' => [
374        'label' => $this->t('Select an action'),
375      ],
376      '#attributes' => [
377        'class' => ['db-background', 'db-menu'],
378        // Require for JavaScript.
379        // @see assets/js/contextual_menu.js
380        'data-db-id' => (string) $builder->id(),
381      ],
382    ];
383
384    $items = [];
385
386    foreach ($islands as $island) {
387      $items = \array_merge($items, $island->build($builder, $builder->getCurrentState()));
388    }
389    $build['#slots']['items'] = $items;
390
391    return $build;
392  }
393
394  /**
395   * Get enabled panes sorted by weight.
396   *
397   * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
398   *   An array of contexts, keyed by context name.
399   *
400   * @return array
401   *   The list of enabled islands sorted.
402   *
403   * @todo just key by weight and default weight in Island?
404   */
405  private function getIslandsEnableSorted(array $contexts): array {
406    // Set island by weight.
407    // @todo just key by weight and default weight in Island?
408    $islands_enable_by_weight = $this->entity->getEnabledIslands();
409
410    return $this->islandPluginManager()->getIslandsByTypes($contexts, $this->entity->getIslandConfigurations(), $islands_enable_by_weight);
411  }
412
413  /**
414   * Gets the entity type manager.
415   *
416   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
417   *   The entity type manager.
418   */
419  private function entityTypeManager(): EntityTypeManagerInterface {
420    if (!isset($this->entityTypeManager)) {
421      $this->entityTypeManager = \Drupal::service('entity_type.manager');
422    }
423
424    return $this->entityTypeManager;
425  }
426
427  /**
428   * Gets the display builder island plugin manager.
429   *
430   * @return \Drupal\display_builder\IslandPluginManagerInterface
431   *   The island plugin manager.
432   */
433  private function islandPluginManager(): IslandPluginManagerInterface {
434    if (!isset($this->islandPluginManager)) {
435      $this->islandPluginManager = \Drupal::service('plugin.manager.db_island');
436    }
437
438    return $this->islandPluginManager;
439  }
440
441}