Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 183 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
ProfileViewBuilder | |
0.00% |
0 / 183 |
|
0.00% |
0 / 11 |
1260 | |
0.00% |
0 / 1 |
view | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
12 | |||
buildSlots | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
90 | |||
prepareViewIslands | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
buildContextualIslands | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
buildPanes | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
buildStartButtons | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
buildBuilderTabs | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
buildMenuWrapper | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getIslandsEnableSorted | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
entityTypeManager | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
islandPluginManager | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Drupal\display_builder; |
6 | |
7 | use Drupal\Core\Entity\EntityInterface; |
8 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
9 | use Drupal\Core\Entity\EntityViewBuilder; |
10 | use Drupal\Core\Security\TrustedCallbackInterface; |
11 | use Drupal\Core\Url; |
12 | |
13 | /** |
14 | * View builder handler for display builder profiles. |
15 | */ |
16 | class 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 | } |