Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
0.00% |
0 / 282 |
|
0.00% |
0 / 106 |
|
0.00% |
0 / 228 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
| ProfileViewBuilder | |
0.00% |
0 / 282 |
|
0.00% |
0 / 106 |
|
0.00% |
0 / 228 |
|
0.00% |
0 / 16 |
2756 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| createInstance | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| view | |
0.00% |
0 / 28 |
|
0.00% |
0 / 7 |
|
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| buildSlots | |
0.00% |
0 / 36 |
|
0.00% |
0 / 18 |
|
0.00% |
0 / 120 |
|
0.00% |
0 / 1 |
90 | |||
| buildFlatLibraryPanels | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| buildButtons | |
0.00% |
0 / 9 |
|
0.00% |
0 / 8 |
|
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
| prepareViewIslands | |
0.00% |
0 / 29 |
|
0.00% |
0 / 13 |
|
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 | |||
| buildContextualIslands | |
0.00% |
0 / 16 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| buildPanes | |
0.00% |
0 / 19 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| buildDeferrableAttribute | |
0.00% |
0 / 1 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
| buildFloatingControlsRegion | |
0.00% |
0 / 39 |
|
0.00% |
0 / 16 |
|
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
56 | |||
| buildStartButtons | |
0.00% |
0 / 23 |
|
0.00% |
0 / 15 |
|
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
| buildDynamicTabs | |
0.00% |
0 / 20 |
|
0.00% |
0 / 9 |
|
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
| resolvePanelDisplay | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| buildMenuWrapper | |
0.00% |
0 / 16 |
|
0.00% |
0 / 4 |
|
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| getIslandsEnableSorted | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder; |
| 6 | |
| 7 | use Drupal\Component\Utility\Html; |
| 8 | use Drupal\Core\Entity\EntityDisplayRepositoryInterface; |
| 9 | use Drupal\Core\Entity\EntityInterface; |
| 10 | use Drupal\Core\Entity\EntityRepositoryInterface; |
| 11 | use Drupal\Core\Entity\EntityTypeInterface; |
| 12 | use Drupal\Core\Entity\EntityTypeManagerInterface; |
| 13 | use Drupal\Core\Entity\EntityViewBuilder; |
| 14 | use Drupal\Core\Language\LanguageManagerInterface; |
| 15 | use Drupal\Core\Security\TrustedCallbackInterface; |
| 16 | use Drupal\Core\Theme\Registry; |
| 17 | use Drupal\display_builder\Entity\ProfileInterface; |
| 18 | use Drupal\display_builder\Island\IslandInterface; |
| 19 | use Drupal\display_builder\Island\IslandPluginManagerInterface; |
| 20 | use Drupal\display_builder\Island\IslandType; |
| 21 | use Symfony\Component\DependencyInjection\ContainerInterface; |
| 22 | |
| 23 | /** |
| 24 | * View builder handler for display builder profiles. |
| 25 | */ |
| 26 | class ProfileViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface { |
| 27 | |
| 28 | use RenderableBuilderTrait; |
| 29 | |
| 30 | /** |
| 31 | * The entity we are building the view for. |
| 32 | */ |
| 33 | protected ProfileInterface $entity; |
| 34 | |
| 35 | /** |
| 36 | * {@inheritdoc} |
| 37 | */ |
| 38 | public function __construct( |
| 39 | EntityTypeInterface $entity_type, |
| 40 | EntityRepositoryInterface $entity_repository, |
| 41 | LanguageManagerInterface $language_manager, |
| 42 | Registry $theme_registry, |
| 43 | EntityDisplayRepositoryInterface $entity_display_repository, |
| 44 | protected EntityTypeManagerInterface $entityTypeManager, |
| 45 | protected IslandPluginManagerInterface $islandPluginManager, |
| 46 | ) { |
| 47 | parent::__construct($entity_type, $entity_repository, $language_manager, $theme_registry, $entity_display_repository); |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * {@inheritdoc} |
| 52 | */ |
| 53 | public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type): static { |
| 54 | return new static( |
| 55 | $entity_type, |
| 56 | $container->get('entity.repository'), |
| 57 | $container->get('language_manager'), |
| 58 | $container->get('theme.registry'), |
| 59 | $container->get('entity_display.repository'), |
| 60 | $container->get('entity_type.manager'), |
| 61 | $container->get('plugin.manager.db_island'), |
| 62 | ); |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * {@inheritdoc} |
| 67 | */ |
| 68 | public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL): array { |
| 69 | // We have 'hacked' the interface by using $view_mode as a way of passing |
| 70 | // the Instance entity ID. |
| 71 | $builder_id = $view_mode; |
| 72 | |
| 73 | /** @var \Drupal\display_builder\Entity\ProfileInterface $entity */ |
| 74 | $entity = $entity; |
| 75 | $this->entity = $entity; |
| 76 | |
| 77 | /** @var \Drupal\display_builder\InstanceInterface $builder */ |
| 78 | $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id); |
| 79 | $buildable_id = $builder->get('buildable')->first()->get('plugin_id')->getValue() ?? ''; |
| 80 | $contexts = $builder->getAvailableContexts() ?? []; |
| 81 | |
| 82 | $islands_enabled_sorted = $this->getIslandsEnableSorted($contexts); |
| 83 | $build = [ |
| 84 | '#type' => 'component', |
| 85 | '#component' => 'display_builder:display_builder', |
| 86 | '#props' => [ |
| 87 | 'builder_id' => $builder_id, |
| 88 | 'buildable_id' => Html::getClass($buildable_id), |
| 89 | 'hash' => (string) $builder->getHash(), |
| 90 | ], |
| 91 | '#slots' => $this->buildSlots($builder, $islands_enabled_sorted), |
| 92 | '#attached' => [], |
| 93 | '#cache' => [ |
| 94 | 'tags' => \array_merge( |
| 95 | $builder->getCacheTags(), |
| 96 | $entity->getCacheTags() |
| 97 | ), |
| 98 | ], |
| 99 | ]; |
| 100 | |
| 101 | foreach ($islands_enabled_sorted as $islands) { |
| 102 | foreach ($islands as $island) { |
| 103 | $build = $island->alterRenderable($builder, $build); |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | return $build; |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Builds and returns the value of each slot. |
| 112 | * |
| 113 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 114 | * Display builder instance. |
| 115 | * @param array $islands_enabled_sorted |
| 116 | * An array of enabled islands. |
| 117 | * |
| 118 | * @return array |
| 119 | * An associative array with the value of each slot. |
| 120 | */ |
| 121 | private function buildSlots(InstanceInterface $builder, array $islands_enabled_sorted): array { |
| 122 | $builder_data = $builder->getCurrentState(); |
| 123 | |
| 124 | $button_islands = $islands_enabled_sorted[IslandType::Button->value] ?? []; |
| 125 | $library_islands = $islands_enabled_sorted[IslandType::Library->value] ?? []; |
| 126 | $contextual_islands = $islands_enabled_sorted[IslandType::Contextual->value] ?? []; |
| 127 | $menu_islands = $islands_enabled_sorted[IslandType::Menu->value] ?? []; |
| 128 | $view_islands = $islands_enabled_sorted[IslandType::View->value] ?? []; |
| 129 | $floating_islands = $islands_enabled_sorted[IslandType::Floating->value] ?? []; |
| 130 | |
| 131 | if (!empty($menu_islands)) { |
| 132 | $menu_islands = $this->buildMenuWrapper($builder, $menu_islands); |
| 133 | } |
| 134 | |
| 135 | if (!empty($library_islands)) { |
| 136 | $library_islands = $this->entity->isLibraryFlat() |
| 137 | ? $this->buildFlatLibraryPanels($builder, $library_islands, $builder_data) |
| 138 | : [ |
| 139 | $this->buildDynamicTabs($builder, $library_islands, TRUE, $this->entity->getLibraryTabsDisplay()), |
| 140 | $this->buildPanes($builder, $library_islands, $builder_data), |
| 141 | ]; |
| 142 | } |
| 143 | |
| 144 | $view_islands_data = $this->prepareViewIslands($builder, $view_islands, $floating_islands); |
| 145 | $view_sidebar = $view_islands_data['view_sidebar']; |
| 146 | $view_main = $view_islands_data['view_main']; |
| 147 | |
| 148 | // Library content can be in main or sidebar. |
| 149 | // @todo Move the logic to LibrariesPanel::build(). |
| 150 | // @see https://www.drupal.org/project/display_builder/issues/3542866 |
| 151 | if (isset($view_sidebar['library']) && !empty($library_islands)) { |
| 152 | $view_sidebar['library']['content'] = $library_islands; |
| 153 | } |
| 154 | elseif (isset($view_main['library']) && !empty($library_islands)) { |
| 155 | $view_main['library']['content'] = $library_islands; |
| 156 | } |
| 157 | |
| 158 | if (!empty($contextual_islands)) { |
| 159 | $contextual_islands = $this->buildContextualIslands($builder, $islands_enabled_sorted); |
| 160 | } |
| 161 | |
| 162 | return [ |
| 163 | 'view_sidebar_buttons' => $view_islands_data['view_sidebar_buttons'], |
| 164 | 'view_sidebar' => $view_sidebar, |
| 165 | 'view_main_tabs' => $view_islands_data['view_main_tabs'], |
| 166 | 'view_main' => $view_main, |
| 167 | 'view_floating_controls' => $view_islands_data['view_floating_controls'], |
| 168 | 'start_buttons' => $this->buildButtons($builder, $button_islands, 'start'), |
| 169 | 'end_buttons' => $this->buildButtons($builder, $button_islands), |
| 170 | 'contextual_islands' => $contextual_islands, |
| 171 | 'menu_islands' => $menu_islands, |
| 172 | ]; |
| 173 | } |
| 174 | |
| 175 | /** |
| 176 | * Build library panels merged into a single flat list with one search box. |
| 177 | * |
| 178 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 179 | * The builder instance. |
| 180 | * @param \Drupal\display_builder\Island\IslandInterface[] $library_islands |
| 181 | * The enabled library islands. |
| 182 | * @param array $builder_data |
| 183 | * The builder current state. |
| 184 | * |
| 185 | * @return array |
| 186 | * A two-element render array: the shared search input, and the panes |
| 187 | * wrapped in a container the search input targets. |
| 188 | */ |
| 189 | private function buildFlatLibraryPanels(InstanceInterface $builder, array $library_islands, array $builder_data): array { |
| 190 | $container_id = 'db-library-flat-' . $builder->id(); |
| 191 | |
| 192 | $search = [ |
| 193 | '#type' => 'component', |
| 194 | '#component' => 'display_builder:input', |
| 195 | '#props' => [ |
| 196 | 'variant' => 'search', |
| 197 | // Names the field for assistive tech, which a placeholder cannot do. |
| 198 | // Visually hidden, @see components/library_panel/search.css. |
| 199 | 'label' => $this->t('Search the library'), |
| 200 | 'placeholder' => $this->t('Search'), |
| 201 | 'size' => 'medium', |
| 202 | 'autocomplete_off' => TRUE, |
| 203 | 'clearable' => TRUE, |
| 204 | 'icon' => 'search', |
| 205 | ], |
| 206 | '#attributes' => [ |
| 207 | 'class' => ['db-search-library'], |
| 208 | 'data-search-container-id' => $container_id, |
| 209 | 'data-elements-selector' => '.db-placeholder', |
| 210 | 'autofocus' => TRUE, |
| 211 | ], |
| 212 | ]; |
| 213 | |
| 214 | $content = [ |
| 215 | '#type' => 'html_tag', |
| 216 | '#tag' => 'div', |
| 217 | '#attributes' => [ |
| 218 | 'id' => $container_id, |
| 219 | 'class' => ['db-library-flat'], |
| 220 | ], |
| 221 | 'panes' => $this->buildPanes($builder, $library_islands, $builder_data), |
| 222 | ]; |
| 223 | |
| 224 | return [$search, $content]; |
| 225 | } |
| 226 | |
| 227 | /** |
| 228 | * Build buttons for a region. |
| 229 | * |
| 230 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 231 | * The builder instance. |
| 232 | * @param \Drupal\display_builder\Island\IslandInterface[] $buttonIslands |
| 233 | * The button islands. |
| 234 | * @param string $region |
| 235 | * The button region. |
| 236 | * |
| 237 | * @return array |
| 238 | * The buttons. |
| 239 | */ |
| 240 | private function buildButtons(InstanceInterface $builder, array $buttonIslands, string $region = 'end'): array { |
| 241 | $islands = []; |
| 242 | |
| 243 | foreach ($buttonIslands as $id => $island) { |
| 244 | $islandRegion = $island->getConfiguration()['region'] ?? 'end'; |
| 245 | |
| 246 | if ($islandRegion === $region) { |
| 247 | $islands[$id] = $island; |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | $buttons = []; |
| 252 | |
| 253 | if (!empty($islands)) { |
| 254 | $buttons = $this->buildPanes($builder, $islands, [], [], 'span'); |
| 255 | } |
| 256 | |
| 257 | return $buttons; |
| 258 | } |
| 259 | |
| 260 | /** |
| 261 | * Prepares view islands data. |
| 262 | * |
| 263 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 264 | * Display builder instance. |
| 265 | * @param array $islands |
| 266 | * The sorted, enabled View islands. |
| 267 | * @param array $floating_islands |
| 268 | * The sorted, enabled Floating islands, to attach to their target main |
| 269 | * View island(s), @see buildFloatingControlsRegion(). |
| 270 | * |
| 271 | * @return array |
| 272 | * The prepared view islands data. |
| 273 | */ |
| 274 | private function prepareViewIslands(InstanceInterface $builder, array $islands, array $floating_islands = []): array { |
| 275 | $view_islands_sidebar = []; |
| 276 | $view_islands_main = []; |
| 277 | $view_sidebar_buttons = []; |
| 278 | $view_main_tabs = []; |
| 279 | |
| 280 | foreach ($islands as $id => $island) { |
| 281 | if ($island->getTypeId() !== IslandType::View->value) { |
| 282 | continue; |
| 283 | } |
| 284 | |
| 285 | $configuration = $island->getConfiguration(); |
| 286 | |
| 287 | if ($configuration['region'] === 'sidebar') { |
| 288 | $view_islands_sidebar[$id] = $islands[$id]; |
| 289 | $view_sidebar_buttons[$id] = $islands[$id]; |
| 290 | } |
| 291 | else { |
| 292 | $view_islands_main[$id] = $islands[$id]; |
| 293 | $view_main_tabs[$id] = $islands[$id]; |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | $view_panels_display = $this->entity->getViewPanelsDisplay(); |
| 298 | |
| 299 | if (!empty($view_sidebar_buttons)) { |
| 300 | $view_sidebar_buttons = $this->buildStartButtons($builder, $view_sidebar_buttons, $view_panels_display); |
| 301 | } |
| 302 | |
| 303 | if (!empty($view_main_tabs)) { |
| 304 | $view_main_tabs = $this->buildDynamicTabs($builder, $view_main_tabs, FALSE, $view_panels_display); |
| 305 | } |
| 306 | |
| 307 | $builder_data = $builder->getCurrentState(); |
| 308 | $view_sidebar = $this->buildPanes($builder, $view_islands_sidebar, $builder_data); |
| 309 | // Default hidden. |
| 310 | $view_main = $this->buildPanes($builder, $view_islands_main, $builder_data, ['shoelace-tabs__tab--hidden']); |
| 311 | $view_floating_controls = $this->buildFloatingControlsRegion($builder, $view_islands_main, $floating_islands); |
| 312 | |
| 313 | return [ |
| 314 | 'view_sidebar_buttons' => $view_sidebar_buttons, |
| 315 | 'view_main_tabs' => $view_main_tabs, |
| 316 | 'view_sidebar' => $view_sidebar, |
| 317 | 'view_main' => $view_main, |
| 318 | 'view_floating_controls' => $view_floating_controls, |
| 319 | ]; |
| 320 | } |
| 321 | |
| 322 | /** |
| 323 | * Build contextual islands which are tabbed sub islands. |
| 324 | * |
| 325 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 326 | * Display builder instance. |
| 327 | * @param array $islands_enabled_sorted |
| 328 | * The islands enabled sorted. |
| 329 | * |
| 330 | * @return array |
| 331 | * The contextual islands render array. |
| 332 | */ |
| 333 | private function buildContextualIslands(InstanceInterface $builder, array $islands_enabled_sorted): array { |
| 334 | $contextual_islands = $islands_enabled_sorted[IslandType::Contextual->value] ?? []; |
| 335 | |
| 336 | if (empty($contextual_islands)) { |
| 337 | return []; |
| 338 | } |
| 339 | |
| 340 | $filter = $this->buildInput((string) $builder->id(), '', 'search', 'medium', 'off', $this->t('Search'), TRUE, 'search'); |
| 341 | // @see components/library_panel/search.js |
| 342 | $filter['#attributes']['class'] = ['db-search-instance']; |
| 343 | |
| 344 | return [ |
| 345 | '#type' => 'html_tag', |
| 346 | '#tag' => 'div', |
| 347 | // Used for custom styling in components/display_builder/form.css. |
| 348 | '#attributes' => [ |
| 349 | 'id' => \sprintf('%s-contextual', $builder->id()), |
| 350 | 'class' => ['db-form'], |
| 351 | ], |
| 352 | 'tabs' => $this->buildDynamicTabs($builder, $contextual_islands, FALSE, $this->entity->getContextualTabsDisplay()), |
| 353 | 'filter' => $filter, |
| 354 | 'panes' => $this->buildPanes($builder, $contextual_islands, $builder->getCurrentState()), |
| 355 | ]; |
| 356 | } |
| 357 | |
| 358 | /** |
| 359 | * Builds panes. |
| 360 | * |
| 361 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 362 | * Display builder instance. |
| 363 | * @param \Drupal\display_builder\Island\IslandInterface[] $islands |
| 364 | * The islands to build tabs for. |
| 365 | * @param array $data |
| 366 | * (Optional) The data to pass to the islands. |
| 367 | * @param array $classes |
| 368 | * (Optional) The HTML classes to start with. |
| 369 | * @param string $tag |
| 370 | * (Optional) The HTML tag, defaults to 'div'. |
| 371 | * |
| 372 | * @return array |
| 373 | * The tabs render array. |
| 374 | */ |
| 375 | private function buildPanes(InstanceInterface $builder, array $islands, array $data = [], array $classes = [], string $tag = 'div'): array { |
| 376 | $panes = []; |
| 377 | |
| 378 | foreach ($islands as $island_id => $island) { |
| 379 | $island_classes = \array_merge($classes, [ |
| 380 | 'db-island', |
| 381 | \sprintf('db-island-%s', $island->getTypeId()), |
| 382 | \sprintf('db-island-%s', $island->getPluginId()), |
| 383 | ]); |
| 384 | |
| 385 | $panes[$island_id] = [ |
| 386 | '#type' => 'html_tag', |
| 387 | '#tag' => $tag, |
| 388 | 'children' => $island->build($builder, $data), |
| 389 | '#attributes' => [ |
| 390 | // `id` attribute is used by HTMX OOB swap. |
| 391 | 'id' => $island->getHtmlId((string) $builder->id()), |
| 392 | // `sse-swap` attribute is used by HTMX SSE swap. |
| 393 | 'sse-swap' => $island->getHtmlId((string) $builder->id()), |
| 394 | 'class' => $island_classes, |
| 395 | 'data-testid' => \sprintf('%s_%s', $island->getTypeId(), $island->getPluginId()), |
| 396 | ] + $this->buildDeferrableAttribute($island), |
| 397 | ]; |
| 398 | } |
| 399 | |
| 400 | return $panes; |
| 401 | } |
| 402 | |
| 403 | /** |
| 404 | * Marks a pane the client may leave stale while it is off screen. |
| 405 | * |
| 406 | * The server owns this decision, so the client does not have to re-derive it |
| 407 | * from island types and stay in sync with them - it just looks for the |
| 408 | * attribute. |
| 409 | * |
| 410 | * @param \Drupal\display_builder\Island\IslandInterface $island |
| 411 | * The island the pane belongs to. |
| 412 | * |
| 413 | * @return array |
| 414 | * The attribute to merge in, or an empty array if the island always |
| 415 | * renders. |
| 416 | * |
| 417 | * @see components/display_builder/js/deferred_islands.js |
| 418 | * @see \Drupal\display_builder\Island\IslandInterface::isDeferrable() |
| 419 | */ |
| 420 | private function buildDeferrableAttribute(IslandInterface $island): array { |
| 421 | return $island->isDeferrable() ? ['data-db-deferrable' => 'true'] : []; |
| 422 | } |
| 423 | |
| 424 | /** |
| 425 | * Builds the floating controls region for the main-region View islands. |
| 426 | * |
| 427 | * Each Floating island is rendered exactly once, as a flex child of a |
| 428 | * single `.db-island-floating-controls` box, no matter how many View panes |
| 429 | * it attaches to. It carries a comma-separated `data-attached-to` listing |
| 430 | * every attached pane present in this profile, and stays visible while ANY |
| 431 | * of them is the active tab (@see components/shoelace/tabs/tabs.js |
| 432 | * syncPanes()). Rendering per Floating island rather than per View pane is |
| 433 | * what keeps its `id` unique - a pane that attached the same Floating island |
| 434 | * twice (e.g. Highlight on both Canvas and Scaffold) used to emit duplicate |
| 435 | * ids, an invalid-HTML/axe failure, and it also keeps the reload/OOB target |
| 436 | * (@see IslandPluginBase::reloadWithGlobalData(), which swaps `#{getHtmlId}`) |
| 437 | * pointing at a single element. |
| 438 | * |
| 439 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 440 | * Display builder instance. |
| 441 | * @param \Drupal\display_builder\Island\IslandInterface[] $view_islands |
| 442 | * The enabled main-region View islands, keyed by plugin ID. |
| 443 | * @param \Drupal\display_builder\Island\IslandInterface[] $floating_islands |
| 444 | * The sorted, enabled Floating islands. |
| 445 | * |
| 446 | * @return array |
| 447 | * A single wrapper render array holding one child per Floating island, or |
| 448 | * an empty array when none attach to a present pane. |
| 449 | */ |
| 450 | private function buildFloatingControlsRegion(InstanceInterface $builder, array $view_islands, array $floating_islands): array { |
| 451 | $data = $builder->getCurrentState(); |
| 452 | $children = []; |
| 453 | |
| 454 | foreach ($floating_islands as $floating_island) { |
| 455 | $definition = $floating_island->getPluginDefinition(); |
| 456 | $attach_to = \is_array($definition) ? ($definition['attach_to'] ?? []) : []; |
| 457 | |
| 458 | // The selectors of the panes this island attaches to that are actually |
| 459 | // present in this profile. The tabs system already uses the same |
| 460 | // selector for a pane's data-target, so syncPanes() shows/hides this |
| 461 | // control in step with those panes. |
| 462 | $targets = []; |
| 463 | |
| 464 | foreach ($attach_to as $view_island_id) { |
| 465 | if (isset($view_islands[$view_island_id])) { |
| 466 | $targets[] = '#' . $view_islands[$view_island_id]->getHtmlId((string) $builder->id()); |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | if (empty($targets)) { |
| 471 | continue; |
| 472 | } |
| 473 | |
| 474 | $floating_island_id = $floating_island->getPluginId(); |
| 475 | // Same db-island-{type}/db-island-{plugin_id} classing as buildPanes() |
| 476 | // - other JS (e.g. assets/js/viewport_switcher.js) selects on it. |
| 477 | $children[$floating_island_id] = [ |
| 478 | '#type' => 'html_tag', |
| 479 | '#tag' => 'div', |
| 480 | '#attributes' => [ |
| 481 | 'id' => $floating_island->getHtmlId((string) $builder->id()), |
| 482 | // `sse-swap` attribute is used by HTMX SSE swap. |
| 483 | 'sse-swap' => $floating_island->getHtmlId((string) $builder->id()), |
| 484 | 'class' => [ |
| 485 | 'db-island', |
| 486 | \sprintf('db-island-%s', $floating_island->getTypeId()), |
| 487 | \sprintf('db-island-%s', $floating_island_id), |
| 488 | // Starts hidden like the panes do; the initial syncPanes() call on |
| 489 | // page load corrects it. |
| 490 | 'shoelace-tabs__tab--hidden', |
| 491 | ], |
| 492 | 'data-testid' => \sprintf('%s_%s', $floating_island->getTypeId(), $floating_island_id), |
| 493 | 'data-attached-to' => \implode(',', $targets), |
| 494 | ] + $this->buildDeferrableAttribute($floating_island), |
| 495 | 'children' => $floating_island->build($builder, $data), |
| 496 | ]; |
| 497 | } |
| 498 | |
| 499 | if (empty($children)) { |
| 500 | return []; |
| 501 | } |
| 502 | |
| 503 | return [ |
| 504 | '#type' => 'html_tag', |
| 505 | '#tag' => 'div', |
| 506 | '#attributes' => [ |
| 507 | 'class' => ['db-island-floating-controls'], |
| 508 | ], |
| 509 | 'children' => $children, |
| 510 | ]; |
| 511 | } |
| 512 | |
| 513 | /** |
| 514 | * Build the buttons to hide/show the drawer. |
| 515 | * |
| 516 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 517 | * Display builder instance. |
| 518 | * @param \Drupal\display_builder\Island\IslandInterface[] $islands |
| 519 | * An array of island objects for which buttons will be created. |
| 520 | * @param string $display |
| 521 | * (Optional) How to show each island: 'label', 'icon' or 'icon_label'. |
| 522 | * Default 'icon_label'. |
| 523 | * |
| 524 | * @return array |
| 525 | * An array of render arrays for the drawer buttons. |
| 526 | */ |
| 527 | private function buildStartButtons(InstanceInterface $builder, array $islands, string $display = 'icon_label'): array { |
| 528 | $build = []; |
| 529 | ['icon' => $show_icon, 'label' => $show_label] = self::resolvePanelDisplay($display); |
| 530 | |
| 531 | foreach ($islands as $island) { |
| 532 | $island_id = $island->getPluginId(); |
| 533 | |
| 534 | $build[$island_id] = [ |
| 535 | '#type' => 'component', |
| 536 | '#component' => 'display_builder:button', |
| 537 | '#props' => [ |
| 538 | 'id' => \sprintf('start-btn-%s-%s', $builder->id(), $island_id), |
| 539 | 'label' => $show_label ? (string) $island->label() : '', |
| 540 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 541 | 'tooltip' => $show_label ? NULL : (string) $island->label(), |
| 542 | 'attributes' => [ |
| 543 | 'data-open-first-drawer' => TRUE, |
| 544 | 'data-target' => $island_id, |
| 545 | ], |
| 546 | ], |
| 547 | ]; |
| 548 | |
| 549 | if ($keyboard = $island::keyboardShortcuts()) { |
| 550 | $build[$island_id]['#attributes']['data-keyboard-key'] = $keyboard['key'] ?? ''; |
| 551 | $build[$island_id]['#attributes']['data-keyboard-help'] = $keyboard['help'] ?? ''; |
| 552 | $build[$island_id]['#attributes']['aria-keyshortcuts'] = $keyboard['key'] ?? ''; |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | return $build; |
| 557 | } |
| 558 | |
| 559 | /** |
| 560 | * Builds tabs. |
| 561 | * |
| 562 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 563 | * Display builder instance. |
| 564 | * @param \Drupal\display_builder\Island\IslandInterface[] $islands |
| 565 | * The islands to build tabs for. |
| 566 | * @param bool $contextual |
| 567 | * (Optional) Is the tabs contextual? See component for details. Default no. |
| 568 | * @param string $display |
| 569 | * (Optional) How to show each island: 'label', 'icon' or 'icon_label'. |
| 570 | * Default 'label'. |
| 571 | * |
| 572 | * @return array |
| 573 | * The tabs render array. |
| 574 | */ |
| 575 | private function buildDynamicTabs(InstanceInterface $builder, array $islands, bool $contextual = FALSE, string $display = 'label'): array { |
| 576 | // Global id is based on last island. |
| 577 | $id = ''; |
| 578 | $tabs = []; |
| 579 | ['icon' => $show_icon, 'label' => $show_label] = self::resolvePanelDisplay($display); |
| 580 | |
| 581 | foreach ($islands as $island) { |
| 582 | $id = $island_id = $island->getHtmlId((string) $builder->id()); |
| 583 | $attributes = [ |
| 584 | 'data-testid' => \sprintf('tab_%s_%s', $island->getTypeId(), $island->getPluginId()), |
| 585 | ]; |
| 586 | |
| 587 | if ($keyboard = $island::keyboardShortcuts()) { |
| 588 | $attributes['data-keyboard-key'] = $keyboard['key'] ?? ''; |
| 589 | $attributes['data-keyboard-help'] = $keyboard['help'] ?? ''; |
| 590 | $attributes['aria-keyshortcuts'] = $keyboard['key'] ?? ''; |
| 591 | } |
| 592 | |
| 593 | $tabs[] = [ |
| 594 | 'title' => $island->label(), |
| 595 | 'url' => '#' . $island_id, |
| 596 | 'attributes' => $attributes, |
| 597 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 598 | 'show_label' => $show_label, |
| 599 | ]; |
| 600 | } |
| 601 | |
| 602 | // Id is needed for storage tabs state, @see component tabs.js file. |
| 603 | return $this->buildTabs($id, $tabs, $contextual); |
| 604 | } |
| 605 | |
| 606 | /** |
| 607 | * Resolves a 'label'/'icon'/'icon_label' display mode into show flags. |
| 608 | * |
| 609 | * @param string $display |
| 610 | * One of 'label', 'icon' or 'icon_label'. |
| 611 | * |
| 612 | * @return array |
| 613 | * An associative array with 'icon' and 'label' boolean flags. |
| 614 | */ |
| 615 | private static function resolvePanelDisplay(string $display): array { |
| 616 | return [ |
| 617 | 'icon' => \in_array($display, ['icon', 'icon_label'], TRUE), |
| 618 | 'label' => \in_array($display, ['label', 'icon_label'], TRUE), |
| 619 | ]; |
| 620 | } |
| 621 | |
| 622 | /** |
| 623 | * Builds menu with islands as entries. |
| 624 | * |
| 625 | * @param \Drupal\display_builder\InstanceInterface $builder |
| 626 | * Display builder instance. |
| 627 | * @param \Drupal\display_builder\Island\IslandInterface[] $islands |
| 628 | * The islands to build tabs for. |
| 629 | * |
| 630 | * @return array |
| 631 | * The islands render array. |
| 632 | * |
| 633 | * @see components/contextual_menu/contextual_menu.js |
| 634 | */ |
| 635 | private function buildMenuWrapper(InstanceInterface $builder, array $islands): array { |
| 636 | $build = [ |
| 637 | '#type' => 'component', |
| 638 | '#component' => 'display_builder:contextual_menu', |
| 639 | '#slots' => [ |
| 640 | 'label' => $this->t('Select an action'), |
| 641 | ], |
| 642 | '#attributes' => [ |
| 643 | 'class' => ['db-background', 'db-menu'], |
| 644 | // Require for JavaScript. |
| 645 | // @see components/contextual_menu/contextual_menu.js |
| 646 | 'data-db-id' => (string) $builder->id(), |
| 647 | ], |
| 648 | ]; |
| 649 | |
| 650 | $items = []; |
| 651 | |
| 652 | foreach ($islands as $island) { |
| 653 | $items = \array_merge($items, $island->build($builder, $builder->getCurrentState())); |
| 654 | } |
| 655 | $build['#slots']['items'] = $items; |
| 656 | |
| 657 | return $build; |
| 658 | } |
| 659 | |
| 660 | /** |
| 661 | * Get enabled panes sorted by weight. |
| 662 | * |
| 663 | * @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts |
| 664 | * An array of contexts, keyed by context name. |
| 665 | * |
| 666 | * @return array |
| 667 | * The list of enabled islands sorted. |
| 668 | * |
| 669 | * @todo just key by weight and default weight in Island? |
| 670 | */ |
| 671 | private function getIslandsEnableSorted(array $contexts): array { |
| 672 | // Set island by weight. |
| 673 | // @todo just key by weight and default weight in Island? |
| 674 | $islands_enable_by_weight = $this->entity->getEnabledIslands(); |
| 675 | |
| 676 | return $this->islandPluginManager->getIslandsByTypes($contexts, $this->entity->getIslandConfigurations(), $islands_enable_by_weight); |
| 677 | } |
| 678 | |
| 679 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 39 | EntityTypeInterface $entity_type, |
| 40 | EntityRepositoryInterface $entity_repository, |
| 41 | LanguageManagerInterface $language_manager, |
| 42 | Registry $theme_registry, |
| 43 | EntityDisplayRepositoryInterface $entity_display_repository, |
| 44 | protected EntityTypeManagerInterface $entityTypeManager, |
| 45 | protected IslandPluginManagerInterface $islandPluginManager, |
| 46 | ) { |
| 47 | parent::__construct($entity_type, $entity_repository, $language_manager, $theme_registry, $entity_display_repository); |
| 48 | } |
| 240 | private function buildButtons(InstanceInterface $builder, array $buttonIslands, string $region = 'end'): array { |
| 241 | $islands = []; |
| 242 | |
| 243 | foreach ($buttonIslands as $id => $island) { |
| 243 | foreach ($buttonIslands as $id => $island) { |
| 243 | foreach ($buttonIslands as $id => $island) { |
| 244 | $islandRegion = $island->getConfiguration()['region'] ?? 'end'; |
| 245 | |
| 246 | if ($islandRegion === $region) { |
| 243 | foreach ($buttonIslands as $id => $island) { |
| 244 | $islandRegion = $island->getConfiguration()['region'] ?? 'end'; |
| 245 | |
| 246 | if ($islandRegion === $region) { |
| 247 | $islands[$id] = $island; |
| 243 | foreach ($buttonIslands as $id => $island) { |
| 243 | foreach ($buttonIslands as $id => $island) { |
| 244 | $islandRegion = $island->getConfiguration()['region'] ?? 'end'; |
| 245 | |
| 246 | if ($islandRegion === $region) { |
| 247 | $islands[$id] = $island; |
| 248 | } |
| 249 | } |
| 250 | |
| 251 | $buttons = []; |
| 252 | |
| 253 | if (!empty($islands)) { |
| 254 | $buttons = $this->buildPanes($builder, $islands, [], [], 'span'); |
| 255 | } |
| 256 | |
| 257 | return $buttons; |
| 257 | return $buttons; |
| 258 | } |
| 333 | private function buildContextualIslands(InstanceInterface $builder, array $islands_enabled_sorted): array { |
| 334 | $contextual_islands = $islands_enabled_sorted[IslandType::Contextual->value] ?? []; |
| 335 | |
| 336 | if (empty($contextual_islands)) { |
| 337 | return []; |
| 340 | $filter = $this->buildInput((string) $builder->id(), '', 'search', 'medium', 'off', $this->t('Search'), TRUE, 'search'); |
| 341 | // @see components/library_panel/search.js |
| 342 | $filter['#attributes']['class'] = ['db-search-instance']; |
| 343 | |
| 344 | return [ |
| 345 | '#type' => 'html_tag', |
| 346 | '#tag' => 'div', |
| 347 | // Used for custom styling in components/display_builder/form.css. |
| 348 | '#attributes' => [ |
| 349 | 'id' => \sprintf('%s-contextual', $builder->id()), |
| 350 | 'class' => ['db-form'], |
| 351 | ], |
| 352 | 'tabs' => $this->buildDynamicTabs($builder, $contextual_islands, FALSE, $this->entity->getContextualTabsDisplay()), |
| 353 | 'filter' => $filter, |
| 354 | 'panes' => $this->buildPanes($builder, $contextual_islands, $builder->getCurrentState()), |
| 355 | ]; |
| 356 | } |
| 420 | private function buildDeferrableAttribute(IslandInterface $island): array { |
| 421 | return $island->isDeferrable() ? ['data-db-deferrable' => 'true'] : []; |
| 421 | return $island->isDeferrable() ? ['data-db-deferrable' => 'true'] : []; |
| 421 | return $island->isDeferrable() ? ['data-db-deferrable' => 'true'] : []; |
| 421 | return $island->isDeferrable() ? ['data-db-deferrable' => 'true'] : []; |
| 422 | } |
| 575 | private function buildDynamicTabs(InstanceInterface $builder, array $islands, bool $contextual = FALSE, string $display = 'label'): array { |
| 576 | // Global id is based on last island. |
| 577 | $id = ''; |
| 578 | $tabs = []; |
| 579 | ['icon' => $show_icon, 'label' => $show_label] = self::resolvePanelDisplay($display); |
| 580 | |
| 581 | foreach ($islands as $island) { |
| 581 | foreach ($islands as $island) { |
| 582 | $id = $island_id = $island->getHtmlId((string) $builder->id()); |
| 583 | $attributes = [ |
| 584 | 'data-testid' => \sprintf('tab_%s_%s', $island->getTypeId(), $island->getPluginId()), |
| 585 | ]; |
| 586 | |
| 587 | if ($keyboard = $island::keyboardShortcuts()) { |
| 588 | $attributes['data-keyboard-key'] = $keyboard['key'] ?? ''; |
| 589 | $attributes['data-keyboard-help'] = $keyboard['help'] ?? ''; |
| 590 | $attributes['aria-keyshortcuts'] = $keyboard['key'] ?? ''; |
| 591 | } |
| 592 | |
| 593 | $tabs[] = [ |
| 594 | 'title' => $island->label(), |
| 594 | 'title' => $island->label(), |
| 595 | 'url' => '#' . $island_id, |
| 596 | 'attributes' => $attributes, |
| 597 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 597 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 594 | 'title' => $island->label(), |
| 581 | foreach ($islands as $island) { |
| 582 | $id = $island_id = $island->getHtmlId((string) $builder->id()); |
| 583 | $attributes = [ |
| 584 | 'data-testid' => \sprintf('tab_%s_%s', $island->getTypeId(), $island->getPluginId()), |
| 585 | ]; |
| 586 | |
| 587 | if ($keyboard = $island::keyboardShortcuts()) { |
| 588 | $attributes['data-keyboard-key'] = $keyboard['key'] ?? ''; |
| 589 | $attributes['data-keyboard-help'] = $keyboard['help'] ?? ''; |
| 590 | $attributes['aria-keyshortcuts'] = $keyboard['key'] ?? ''; |
| 591 | } |
| 592 | |
| 593 | $tabs[] = [ |
| 594 | 'title' => $island->label(), |
| 581 | foreach ($islands as $island) { |
| 582 | $id = $island_id = $island->getHtmlId((string) $builder->id()); |
| 583 | $attributes = [ |
| 584 | 'data-testid' => \sprintf('tab_%s_%s', $island->getTypeId(), $island->getPluginId()), |
| 585 | ]; |
| 586 | |
| 587 | if ($keyboard = $island::keyboardShortcuts()) { |
| 588 | $attributes['data-keyboard-key'] = $keyboard['key'] ?? ''; |
| 589 | $attributes['data-keyboard-help'] = $keyboard['help'] ?? ''; |
| 590 | $attributes['aria-keyshortcuts'] = $keyboard['key'] ?? ''; |
| 591 | } |
| 592 | |
| 593 | $tabs[] = [ |
| 594 | 'title' => $island->label(), |
| 595 | 'url' => '#' . $island_id, |
| 596 | 'attributes' => $attributes, |
| 597 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 598 | 'show_label' => $show_label, |
| 599 | ]; |
| 600 | } |
| 601 | |
| 602 | // Id is needed for storage tabs state, @see component tabs.js file. |
| 603 | return $this->buildTabs($id, $tabs, $contextual); |
| 604 | } |
| 189 | private function buildFlatLibraryPanels(InstanceInterface $builder, array $library_islands, array $builder_data): array { |
| 190 | $container_id = 'db-library-flat-' . $builder->id(); |
| 191 | |
| 192 | $search = [ |
| 193 | '#type' => 'component', |
| 194 | '#component' => 'display_builder:input', |
| 195 | '#props' => [ |
| 196 | 'variant' => 'search', |
| 197 | // Names the field for assistive tech, which a placeholder cannot do. |
| 198 | // Visually hidden, @see components/library_panel/search.css. |
| 199 | 'label' => $this->t('Search the library'), |
| 200 | 'placeholder' => $this->t('Search'), |
| 201 | 'size' => 'medium', |
| 202 | 'autocomplete_off' => TRUE, |
| 203 | 'clearable' => TRUE, |
| 204 | 'icon' => 'search', |
| 205 | ], |
| 206 | '#attributes' => [ |
| 207 | 'class' => ['db-search-library'], |
| 208 | 'data-search-container-id' => $container_id, |
| 209 | 'data-elements-selector' => '.db-placeholder', |
| 210 | 'autofocus' => TRUE, |
| 211 | ], |
| 212 | ]; |
| 213 | |
| 214 | $content = [ |
| 215 | '#type' => 'html_tag', |
| 216 | '#tag' => 'div', |
| 217 | '#attributes' => [ |
| 218 | 'id' => $container_id, |
| 219 | 'class' => ['db-library-flat'], |
| 220 | ], |
| 221 | 'panes' => $this->buildPanes($builder, $library_islands, $builder_data), |
| 222 | ]; |
| 223 | |
| 224 | return [$search, $content]; |
| 225 | } |
| 450 | private function buildFloatingControlsRegion(InstanceInterface $builder, array $view_islands, array $floating_islands): array { |
| 451 | $data = $builder->getCurrentState(); |
| 452 | $children = []; |
| 453 | |
| 454 | foreach ($floating_islands as $floating_island) { |
| 454 | foreach ($floating_islands as $floating_island) { |
| 455 | $definition = $floating_island->getPluginDefinition(); |
| 456 | $attach_to = \is_array($definition) ? ($definition['attach_to'] ?? []) : []; |
| 456 | $attach_to = \is_array($definition) ? ($definition['attach_to'] ?? []) : []; |
| 456 | $attach_to = \is_array($definition) ? ($definition['attach_to'] ?? []) : []; |
| 456 | $attach_to = \is_array($definition) ? ($definition['attach_to'] ?? []) : []; |
| 457 | |
| 458 | // The selectors of the panes this island attaches to that are actually |
| 459 | // present in this profile. The tabs system already uses the same |
| 460 | // selector for a pane's data-target, so syncPanes() shows/hides this |
| 461 | // control in step with those panes. |
| 462 | $targets = []; |
| 463 | |
| 464 | foreach ($attach_to as $view_island_id) { |
| 464 | foreach ($attach_to as $view_island_id) { |
| 465 | if (isset($view_islands[$view_island_id])) { |
| 464 | foreach ($attach_to as $view_island_id) { |
| 465 | if (isset($view_islands[$view_island_id])) { |
| 466 | $targets[] = '#' . $view_islands[$view_island_id]->getHtmlId((string) $builder->id()); |
| 464 | foreach ($attach_to as $view_island_id) { |
| 464 | foreach ($attach_to as $view_island_id) { |
| 465 | if (isset($view_islands[$view_island_id])) { |
| 466 | $targets[] = '#' . $view_islands[$view_island_id]->getHtmlId((string) $builder->id()); |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | if (empty($targets)) { |
| 471 | continue; |
| 454 | foreach ($floating_islands as $floating_island) { |
| 455 | $definition = $floating_island->getPluginDefinition(); |
| 456 | $attach_to = \is_array($definition) ? ($definition['attach_to'] ?? []) : []; |
| 457 | |
| 458 | // The selectors of the panes this island attaches to that are actually |
| 459 | // present in this profile. The tabs system already uses the same |
| 460 | // selector for a pane's data-target, so syncPanes() shows/hides this |
| 461 | // control in step with those panes. |
| 462 | $targets = []; |
| 463 | |
| 464 | foreach ($attach_to as $view_island_id) { |
| 465 | if (isset($view_islands[$view_island_id])) { |
| 466 | $targets[] = '#' . $view_islands[$view_island_id]->getHtmlId((string) $builder->id()); |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | if (empty($targets)) { |
| 471 | continue; |
| 472 | } |
| 473 | |
| 474 | $floating_island_id = $floating_island->getPluginId(); |
| 454 | foreach ($floating_islands as $floating_island) { |
| 455 | $definition = $floating_island->getPluginDefinition(); |
| 456 | $attach_to = \is_array($definition) ? ($definition['attach_to'] ?? []) : []; |
| 457 | |
| 458 | // The selectors of the panes this island attaches to that are actually |
| 459 | // present in this profile. The tabs system already uses the same |
| 460 | // selector for a pane's data-target, so syncPanes() shows/hides this |
| 461 | // control in step with those panes. |
| 462 | $targets = []; |
| 463 | |
| 464 | foreach ($attach_to as $view_island_id) { |
| 465 | if (isset($view_islands[$view_island_id])) { |
| 466 | $targets[] = '#' . $view_islands[$view_island_id]->getHtmlId((string) $builder->id()); |
| 467 | } |
| 468 | } |
| 469 | |
| 470 | if (empty($targets)) { |
| 471 | continue; |
| 472 | } |
| 473 | |
| 474 | $floating_island_id = $floating_island->getPluginId(); |
| 475 | // Same db-island-{type}/db-island-{plugin_id} classing as buildPanes() |
| 476 | // - other JS (e.g. assets/js/viewport_switcher.js) selects on it. |
| 477 | $children[$floating_island_id] = [ |
| 478 | '#type' => 'html_tag', |
| 479 | '#tag' => 'div', |
| 480 | '#attributes' => [ |
| 481 | 'id' => $floating_island->getHtmlId((string) $builder->id()), |
| 482 | // `sse-swap` attribute is used by HTMX SSE swap. |
| 483 | 'sse-swap' => $floating_island->getHtmlId((string) $builder->id()), |
| 484 | 'class' => [ |
| 485 | 'db-island', |
| 486 | \sprintf('db-island-%s', $floating_island->getTypeId()), |
| 487 | \sprintf('db-island-%s', $floating_island_id), |
| 488 | // Starts hidden like the panes do; the initial syncPanes() call on |
| 489 | // page load corrects it. |
| 490 | 'shoelace-tabs__tab--hidden', |
| 491 | ], |
| 492 | 'data-testid' => \sprintf('%s_%s', $floating_island->getTypeId(), $floating_island_id), |
| 493 | 'data-attached-to' => \implode(',', $targets), |
| 494 | ] + $this->buildDeferrableAttribute($floating_island), |
| 495 | 'children' => $floating_island->build($builder, $data), |
| 496 | ]; |
| 497 | } |
| 498 | |
| 499 | if (empty($children)) { |
| 500 | return []; |
| 504 | '#type' => 'html_tag', |
| 505 | '#tag' => 'div', |
| 506 | '#attributes' => [ |
| 507 | 'class' => ['db-island-floating-controls'], |
| 508 | ], |
| 509 | 'children' => $children, |
| 510 | ]; |
| 511 | } |
| 635 | private function buildMenuWrapper(InstanceInterface $builder, array $islands): array { |
| 636 | $build = [ |
| 637 | '#type' => 'component', |
| 638 | '#component' => 'display_builder:contextual_menu', |
| 639 | '#slots' => [ |
| 640 | 'label' => $this->t('Select an action'), |
| 641 | ], |
| 642 | '#attributes' => [ |
| 643 | 'class' => ['db-background', 'db-menu'], |
| 644 | // Require for JavaScript. |
| 645 | // @see components/contextual_menu/contextual_menu.js |
| 646 | 'data-db-id' => (string) $builder->id(), |
| 647 | ], |
| 648 | ]; |
| 649 | |
| 650 | $items = []; |
| 651 | |
| 652 | foreach ($islands as $island) { |
| 652 | foreach ($islands as $island) { |
| 652 | foreach ($islands as $island) { |
| 653 | $items = \array_merge($items, $island->build($builder, $builder->getCurrentState())); |
| 652 | foreach ($islands as $island) { |
| 653 | $items = \array_merge($items, $island->build($builder, $builder->getCurrentState())); |
| 654 | } |
| 655 | $build['#slots']['items'] = $items; |
| 656 | |
| 657 | return $build; |
| 658 | } |
| 375 | private function buildPanes(InstanceInterface $builder, array $islands, array $data = [], array $classes = [], string $tag = 'div'): array { |
| 376 | $panes = []; |
| 377 | |
| 378 | foreach ($islands as $island_id => $island) { |
| 378 | foreach ($islands as $island_id => $island) { |
| 378 | foreach ($islands as $island_id => $island) { |
| 378 | foreach ($islands as $island_id => $island) { |
| 379 | $island_classes = \array_merge($classes, [ |
| 380 | 'db-island', |
| 381 | \sprintf('db-island-%s', $island->getTypeId()), |
| 382 | \sprintf('db-island-%s', $island->getPluginId()), |
| 383 | ]); |
| 384 | |
| 385 | $panes[$island_id] = [ |
| 386 | '#type' => 'html_tag', |
| 387 | '#tag' => $tag, |
| 388 | 'children' => $island->build($builder, $data), |
| 389 | '#attributes' => [ |
| 390 | // `id` attribute is used by HTMX OOB swap. |
| 391 | 'id' => $island->getHtmlId((string) $builder->id()), |
| 392 | // `sse-swap` attribute is used by HTMX SSE swap. |
| 393 | 'sse-swap' => $island->getHtmlId((string) $builder->id()), |
| 394 | 'class' => $island_classes, |
| 395 | 'data-testid' => \sprintf('%s_%s', $island->getTypeId(), $island->getPluginId()), |
| 396 | ] + $this->buildDeferrableAttribute($island), |
| 397 | ]; |
| 398 | } |
| 399 | |
| 400 | return $panes; |
| 401 | } |
| 121 | private function buildSlots(InstanceInterface $builder, array $islands_enabled_sorted): array { |
| 122 | $builder_data = $builder->getCurrentState(); |
| 123 | |
| 124 | $button_islands = $islands_enabled_sorted[IslandType::Button->value] ?? []; |
| 125 | $library_islands = $islands_enabled_sorted[IslandType::Library->value] ?? []; |
| 126 | $contextual_islands = $islands_enabled_sorted[IslandType::Contextual->value] ?? []; |
| 127 | $menu_islands = $islands_enabled_sorted[IslandType::Menu->value] ?? []; |
| 128 | $view_islands = $islands_enabled_sorted[IslandType::View->value] ?? []; |
| 129 | $floating_islands = $islands_enabled_sorted[IslandType::Floating->value] ?? []; |
| 130 | |
| 131 | if (!empty($menu_islands)) { |
| 132 | $menu_islands = $this->buildMenuWrapper($builder, $menu_islands); |
| 133 | } |
| 134 | |
| 135 | if (!empty($library_islands)) { |
| 135 | if (!empty($library_islands)) { |
| 136 | $library_islands = $this->entity->isLibraryFlat() |
| 137 | ? $this->buildFlatLibraryPanels($builder, $library_islands, $builder_data) |
| 136 | $library_islands = $this->entity->isLibraryFlat() |
| 137 | ? $this->buildFlatLibraryPanels($builder, $library_islands, $builder_data) |
| 138 | : [ |
| 139 | $this->buildDynamicTabs($builder, $library_islands, TRUE, $this->entity->getLibraryTabsDisplay()), |
| 136 | $library_islands = $this->entity->isLibraryFlat() |
| 137 | ? $this->buildFlatLibraryPanels($builder, $library_islands, $builder_data) |
| 138 | : [ |
| 139 | $this->buildDynamicTabs($builder, $library_islands, TRUE, $this->entity->getLibraryTabsDisplay()), |
| 140 | $this->buildPanes($builder, $library_islands, $builder_data), |
| 141 | ]; |
| 142 | } |
| 143 | |
| 144 | $view_islands_data = $this->prepareViewIslands($builder, $view_islands, $floating_islands); |
| 144 | $view_islands_data = $this->prepareViewIslands($builder, $view_islands, $floating_islands); |
| 145 | $view_sidebar = $view_islands_data['view_sidebar']; |
| 146 | $view_main = $view_islands_data['view_main']; |
| 147 | |
| 148 | // Library content can be in main or sidebar. |
| 149 | // @todo Move the logic to LibrariesPanel::build(). |
| 150 | // @see https://www.drupal.org/project/display_builder/issues/3542866 |
| 151 | if (isset($view_sidebar['library']) && !empty($library_islands)) { |
| 151 | if (isset($view_sidebar['library']) && !empty($library_islands)) { |
| 151 | if (isset($view_sidebar['library']) && !empty($library_islands)) { |
| 151 | if (isset($view_sidebar['library']) && !empty($library_islands)) { |
| 152 | $view_sidebar['library']['content'] = $library_islands; |
| 154 | elseif (isset($view_main['library']) && !empty($library_islands)) { |
| 154 | elseif (isset($view_main['library']) && !empty($library_islands)) { |
| 154 | elseif (isset($view_main['library']) && !empty($library_islands)) { |
| 155 | $view_main['library']['content'] = $library_islands; |
| 156 | } |
| 157 | |
| 158 | if (!empty($contextual_islands)) { |
| 158 | if (!empty($contextual_islands)) { |
| 159 | $contextual_islands = $this->buildContextualIslands($builder, $islands_enabled_sorted); |
| 160 | } |
| 161 | |
| 162 | return [ |
| 163 | 'view_sidebar_buttons' => $view_islands_data['view_sidebar_buttons'], |
| 163 | 'view_sidebar_buttons' => $view_islands_data['view_sidebar_buttons'], |
| 164 | 'view_sidebar' => $view_sidebar, |
| 165 | 'view_main_tabs' => $view_islands_data['view_main_tabs'], |
| 166 | 'view_main' => $view_main, |
| 167 | 'view_floating_controls' => $view_islands_data['view_floating_controls'], |
| 168 | 'start_buttons' => $this->buildButtons($builder, $button_islands, 'start'), |
| 169 | 'end_buttons' => $this->buildButtons($builder, $button_islands), |
| 170 | 'contextual_islands' => $contextual_islands, |
| 171 | 'menu_islands' => $menu_islands, |
| 172 | ]; |
| 173 | } |
| 527 | private function buildStartButtons(InstanceInterface $builder, array $islands, string $display = 'icon_label'): array { |
| 528 | $build = []; |
| 529 | ['icon' => $show_icon, 'label' => $show_label] = self::resolvePanelDisplay($display); |
| 530 | |
| 531 | foreach ($islands as $island) { |
| 531 | foreach ($islands as $island) { |
| 532 | $island_id = $island->getPluginId(); |
| 533 | |
| 534 | $build[$island_id] = [ |
| 535 | '#type' => 'component', |
| 536 | '#component' => 'display_builder:button', |
| 537 | '#props' => [ |
| 538 | 'id' => \sprintf('start-btn-%s-%s', $builder->id(), $island_id), |
| 539 | 'label' => $show_label ? (string) $island->label() : '', |
| 539 | 'label' => $show_label ? (string) $island->label() : '', |
| 539 | 'label' => $show_label ? (string) $island->label() : '', |
| 539 | 'label' => $show_label ? (string) $island->label() : '', |
| 540 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 540 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 535 | '#type' => 'component', |
| 535 | '#type' => 'component', |
| 536 | '#component' => 'display_builder:button', |
| 537 | '#props' => [ |
| 538 | 'id' => \sprintf('start-btn-%s-%s', $builder->id(), $island_id), |
| 539 | 'label' => $show_label ? (string) $island->label() : '', |
| 540 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 541 | 'tooltip' => $show_label ? NULL : (string) $island->label(), |
| 535 | '#type' => 'component', |
| 541 | 'tooltip' => $show_label ? NULL : (string) $island->label(), |
| 541 | 'tooltip' => $show_label ? NULL : (string) $island->label(), |
| 542 | 'attributes' => [ |
| 543 | 'data-open-first-drawer' => TRUE, |
| 544 | 'data-target' => $island_id, |
| 545 | ], |
| 546 | ], |
| 547 | ]; |
| 548 | |
| 549 | if ($keyboard = $island::keyboardShortcuts()) { |
| 531 | foreach ($islands as $island) { |
| 532 | $island_id = $island->getPluginId(); |
| 533 | |
| 534 | $build[$island_id] = [ |
| 535 | '#type' => 'component', |
| 536 | '#component' => 'display_builder:button', |
| 537 | '#props' => [ |
| 538 | 'id' => \sprintf('start-btn-%s-%s', $builder->id(), $island_id), |
| 539 | 'label' => $show_label ? (string) $island->label() : '', |
| 540 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 541 | 'tooltip' => $show_label ? NULL : (string) $island->label(), |
| 542 | 'attributes' => [ |
| 543 | 'data-open-first-drawer' => TRUE, |
| 544 | 'data-target' => $island_id, |
| 545 | ], |
| 546 | ], |
| 547 | ]; |
| 548 | |
| 549 | if ($keyboard = $island::keyboardShortcuts()) { |
| 550 | $build[$island_id]['#attributes']['data-keyboard-key'] = $keyboard['key'] ?? ''; |
| 531 | foreach ($islands as $island) { |
| 531 | foreach ($islands as $island) { |
| 532 | $island_id = $island->getPluginId(); |
| 533 | |
| 534 | $build[$island_id] = [ |
| 535 | '#type' => 'component', |
| 536 | '#component' => 'display_builder:button', |
| 537 | '#props' => [ |
| 538 | 'id' => \sprintf('start-btn-%s-%s', $builder->id(), $island_id), |
| 539 | 'label' => $show_label ? (string) $island->label() : '', |
| 540 | 'icon' => $show_icon ? $island->getIcon() : NULL, |
| 541 | 'tooltip' => $show_label ? NULL : (string) $island->label(), |
| 542 | 'attributes' => [ |
| 543 | 'data-open-first-drawer' => TRUE, |
| 544 | 'data-target' => $island_id, |
| 545 | ], |
| 546 | ], |
| 547 | ]; |
| 548 | |
| 549 | if ($keyboard = $island::keyboardShortcuts()) { |
| 550 | $build[$island_id]['#attributes']['data-keyboard-key'] = $keyboard['key'] ?? ''; |
| 551 | $build[$island_id]['#attributes']['data-keyboard-help'] = $keyboard['help'] ?? ''; |
| 552 | $build[$island_id]['#attributes']['aria-keyshortcuts'] = $keyboard['key'] ?? ''; |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | return $build; |
| 557 | } |
| 53 | public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type): static { |
| 54 | return new static( |
| 55 | $entity_type, |
| 56 | $container->get('entity.repository'), |
| 57 | $container->get('language_manager'), |
| 58 | $container->get('theme.registry'), |
| 59 | $container->get('entity_display.repository'), |
| 60 | $container->get('entity_type.manager'), |
| 61 | $container->get('plugin.manager.db_island'), |
| 62 | ); |
| 63 | } |
| 671 | private function getIslandsEnableSorted(array $contexts): array { |
| 672 | // Set island by weight. |
| 673 | // @todo just key by weight and default weight in Island? |
| 674 | $islands_enable_by_weight = $this->entity->getEnabledIslands(); |
| 675 | |
| 676 | return $this->islandPluginManager->getIslandsByTypes($contexts, $this->entity->getIslandConfigurations(), $islands_enable_by_weight); |
| 677 | } |
| 274 | private function prepareViewIslands(InstanceInterface $builder, array $islands, array $floating_islands = []): array { |
| 275 | $view_islands_sidebar = []; |
| 276 | $view_islands_main = []; |
| 277 | $view_sidebar_buttons = []; |
| 278 | $view_main_tabs = []; |
| 279 | |
| 280 | foreach ($islands as $id => $island) { |
| 280 | foreach ($islands as $id => $island) { |
| 280 | foreach ($islands as $id => $island) { |
| 281 | if ($island->getTypeId() !== IslandType::View->value) { |
| 282 | continue; |
| 285 | $configuration = $island->getConfiguration(); |
| 286 | |
| 287 | if ($configuration['region'] === 'sidebar') { |
| 287 | if ($configuration['region'] === 'sidebar') { |
| 288 | $view_islands_sidebar[$id] = $islands[$id]; |
| 280 | foreach ($islands as $id => $island) { |
| 281 | if ($island->getTypeId() !== IslandType::View->value) { |
| 282 | continue; |
| 283 | } |
| 284 | |
| 285 | $configuration = $island->getConfiguration(); |
| 286 | |
| 287 | if ($configuration['region'] === 'sidebar') { |
| 288 | $view_islands_sidebar[$id] = $islands[$id]; |
| 289 | $view_sidebar_buttons[$id] = $islands[$id]; |
| 290 | } |
| 291 | else { |
| 292 | $view_islands_main[$id] = $islands[$id]; |
| 280 | foreach ($islands as $id => $island) { |
| 280 | foreach ($islands as $id => $island) { |
| 281 | if ($island->getTypeId() !== IslandType::View->value) { |
| 282 | continue; |
| 283 | } |
| 284 | |
| 285 | $configuration = $island->getConfiguration(); |
| 286 | |
| 287 | if ($configuration['region'] === 'sidebar') { |
| 288 | $view_islands_sidebar[$id] = $islands[$id]; |
| 289 | $view_sidebar_buttons[$id] = $islands[$id]; |
| 290 | } |
| 291 | else { |
| 292 | $view_islands_main[$id] = $islands[$id]; |
| 293 | $view_main_tabs[$id] = $islands[$id]; |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | $view_panels_display = $this->entity->getViewPanelsDisplay(); |
| 298 | |
| 299 | if (!empty($view_sidebar_buttons)) { |
| 300 | $view_sidebar_buttons = $this->buildStartButtons($builder, $view_sidebar_buttons, $view_panels_display); |
| 301 | } |
| 302 | |
| 303 | if (!empty($view_main_tabs)) { |
| 303 | if (!empty($view_main_tabs)) { |
| 304 | $view_main_tabs = $this->buildDynamicTabs($builder, $view_main_tabs, FALSE, $view_panels_display); |
| 305 | } |
| 306 | |
| 307 | $builder_data = $builder->getCurrentState(); |
| 307 | $builder_data = $builder->getCurrentState(); |
| 308 | $view_sidebar = $this->buildPanes($builder, $view_islands_sidebar, $builder_data); |
| 309 | // Default hidden. |
| 310 | $view_main = $this->buildPanes($builder, $view_islands_main, $builder_data, ['shoelace-tabs__tab--hidden']); |
| 311 | $view_floating_controls = $this->buildFloatingControlsRegion($builder, $view_islands_main, $floating_islands); |
| 312 | |
| 313 | return [ |
| 314 | 'view_sidebar_buttons' => $view_sidebar_buttons, |
| 315 | 'view_main_tabs' => $view_main_tabs, |
| 316 | 'view_sidebar' => $view_sidebar, |
| 317 | 'view_main' => $view_main, |
| 318 | 'view_floating_controls' => $view_floating_controls, |
| 319 | ]; |
| 320 | } |
| 615 | private static function resolvePanelDisplay(string $display): array { |
| 616 | return [ |
| 617 | 'icon' => \in_array($display, ['icon', 'icon_label'], TRUE), |
| 618 | 'label' => \in_array($display, ['label', 'icon_label'], TRUE), |
| 619 | ]; |
| 620 | } |
| 68 | public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL): array { |
| 69 | // We have 'hacked' the interface by using $view_mode as a way of passing |
| 70 | // the Instance entity ID. |
| 71 | $builder_id = $view_mode; |
| 72 | |
| 73 | /** @var \Drupal\display_builder\Entity\ProfileInterface $entity */ |
| 74 | $entity = $entity; |
| 75 | $this->entity = $entity; |
| 76 | |
| 77 | /** @var \Drupal\display_builder\InstanceInterface $builder */ |
| 78 | $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id); |
| 79 | $buildable_id = $builder->get('buildable')->first()->get('plugin_id')->getValue() ?? ''; |
| 80 | $contexts = $builder->getAvailableContexts() ?? []; |
| 81 | |
| 82 | $islands_enabled_sorted = $this->getIslandsEnableSorted($contexts); |
| 83 | $build = [ |
| 84 | '#type' => 'component', |
| 85 | '#component' => 'display_builder:display_builder', |
| 86 | '#props' => [ |
| 87 | 'builder_id' => $builder_id, |
| 88 | 'buildable_id' => Html::getClass($buildable_id), |
| 89 | 'hash' => (string) $builder->getHash(), |
| 90 | ], |
| 91 | '#slots' => $this->buildSlots($builder, $islands_enabled_sorted), |
| 92 | '#attached' => [], |
| 93 | '#cache' => [ |
| 94 | 'tags' => \array_merge( |
| 95 | $builder->getCacheTags(), |
| 96 | $entity->getCacheTags() |
| 97 | ), |
| 98 | ], |
| 99 | ]; |
| 100 | |
| 101 | foreach ($islands_enabled_sorted as $islands) { |
| 101 | foreach ($islands_enabled_sorted as $islands) { |
| 102 | foreach ($islands as $island) { |
| 102 | foreach ($islands as $island) { |
| 102 | foreach ($islands as $island) { |
| 103 | $build = $island->alterRenderable($builder, $build); |
| 101 | foreach ($islands_enabled_sorted as $islands) { |
| 102 | foreach ($islands as $island) { |
| 101 | foreach ($islands_enabled_sorted as $islands) { |
| 102 | foreach ($islands as $island) { |
| 103 | $build = $island->alterRenderable($builder, $build); |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | return $build; |
| 108 | } |