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