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