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