Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
191 / 191 |
|
100.00% |
71 / 71 |
|
26.92% |
35 / 130 |
|
100.00% |
16 / 16 |
CRAP | |
100.00% |
1 / 1 |
| RenderableBuilderTrait | |
100.00% |
191 / 191 |
|
100.00% |
71 / 71 |
|
26.92% |
35 / 130 |
|
100.00% |
16 / 16 |
764.57 | |
100.00% |
1 / 1 |
| buildError | |
100.00% |
27 / 27 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| isRenderEmptyOrFailing | |
100.00% |
4 / 4 |
|
100.00% |
4 / 4 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| buildPlaceholder | |
100.00% |
26 / 26 |
|
100.00% |
15 / 15 |
|
7.81% |
5 / 64 |
|
100.00% |
1 / 1 |
58.14 | |||
| buildPlaceholderButton | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| buildPlaceholderList | |
100.00% |
6 / 6 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| buildPlaceholderListWithPreview | |
100.00% |
5 / 5 |
|
100.00% |
7 / 7 |
|
50.00% |
3 / 6 |
|
100.00% |
1 / 1 |
6.00 | |||
| buildPlaceholderCard | |
100.00% |
12 / 12 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| buildPlaceholderCardWithPreview | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| buildButton | |
100.00% |
17 / 17 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| buildMenuItem | |
100.00% |
17 / 17 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| buildMenuDivider | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| buildDraggables | |
100.00% |
13 / 13 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| buildTabs | |
100.00% |
11 / 11 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| buildInput | |
100.00% |
20 / 20 |
|
100.00% |
11 / 11 |
|
6.25% |
2 / 32 |
|
100.00% |
1 / 1 |
35.66 | |||
| wrapContent | |
100.00% |
8 / 8 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| applyPreview | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Drupal\display_builder; |
| 6 | |
| 7 | use Drupal\Core\Htmx\Htmx; |
| 8 | use Drupal\Core\Render\RendererInterface; |
| 9 | use Drupal\Core\StringTranslation\TranslatableMarkup; |
| 10 | use Drupal\Core\Url; |
| 11 | |
| 12 | /** |
| 13 | * Trait with helpers to build renderables. |
| 14 | */ |
| 15 | trait RenderableBuilderTrait { |
| 16 | |
| 17 | /** |
| 18 | * Build error message. |
| 19 | * |
| 20 | * @param string $builder_id |
| 21 | * The builder id. |
| 22 | * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message |
| 23 | * The message to display. |
| 24 | * @param bool $global |
| 25 | * (Optional) Try to use the default builder toast stack. |
| 26 | * @param int|null $duration |
| 27 | * (Optional) Alert duration before closing. |
| 28 | * |
| 29 | * @return array |
| 30 | * The input render array. |
| 31 | */ |
| 32 | public function buildError(string $builder_id, string|TranslatableMarkup $message, bool $global = FALSE, ?int $duration = NULL): array { |
| 33 | $build = [ |
| 34 | '#type' => 'component', |
| 35 | '#component' => 'display_builder:alert', |
| 36 | '#slots' => [ |
| 37 | 'content' => $message, |
| 38 | ], |
| 39 | '#props' => [ |
| 40 | 'variant' => 'danger', |
| 41 | 'icon' => 'exclamation-octagon', |
| 42 | 'open' => TRUE, |
| 43 | 'closable' => TRUE, |
| 44 | ], |
| 45 | '#attributes' => [ |
| 46 | 'class' => 'db-message', |
| 47 | ], |
| 48 | ]; |
| 49 | |
| 50 | if ($duration) { |
| 51 | $build['#props']['duration'] = $duration; |
| 52 | } |
| 53 | |
| 54 | if ($global) { |
| 55 | // Append into the builder's toast stack rather than replacing whatever |
| 56 | // is already on screen: a "beforeend" out-of-band swap keeps previous |
| 57 | // messages visible, so a burst of errors stacks instead of each one |
| 58 | // silently overwriting the last. htmx swaps the *content* of the |
| 59 | // out-of-band element for any non-inline swap style, hence the wrapper. |
| 60 | $build = [ |
| 61 | '#type' => 'container', |
| 62 | 'message' => $build, |
| 63 | ]; |
| 64 | (new Htmx()) |
| 65 | ->swapOob(\sprintf('beforeend:#message-%s', $builder_id)) |
| 66 | ->applyTo($build); |
| 67 | } |
| 68 | |
| 69 | return $build; |
| 70 | } |
| 71 | |
| 72 | /** |
| 73 | * Checks whether a renderable array is empty, or throws once rendered. |
| 74 | * |
| 75 | * Some render arrays (e.g. a comment field formatter's "Add comment" form |
| 76 | * #lazy_builder placeholder, built against an unsaved sample entity with |
| 77 | * no real ID - @see \Drupal\display_builder_entity_view\Plugin\display_builder\Buildable\EntityView) |
| 78 | * throw rather than produce empty markup when actually rendered. Checking |
| 79 | * with renderInIsolation() here, synchronously, keeps a bad lazy_builder |
| 80 | * from surviving unresolved into the *caller's own* render array, where - |
| 81 | * left unrendered - it would only fail later during Drupal core's own |
| 82 | * BigPipe processing, well outside of this try/catch's reach, breaking |
| 83 | * page rendering entirely instead of degrading gracefully to a |
| 84 | * placeholder. |
| 85 | * |
| 86 | * @param \Drupal\Core\Render\RendererInterface $renderer |
| 87 | * The renderer. |
| 88 | * @param array $renderable |
| 89 | * The renderable array to check. |
| 90 | * |
| 91 | * @return bool |
| 92 | * TRUE if the renderable is empty or fails to render, FALSE otherwise. |
| 93 | */ |
| 94 | protected function isRenderEmptyOrFailing(RendererInterface $renderer, array $renderable): bool { |
| 95 | try { |
| 96 | $html = $renderer->renderInIsolation($renderable); |
| 97 | } |
| 98 | catch (\Throwable $e) { |
| 99 | return TRUE; |
| 100 | } |
| 101 | |
| 102 | return empty(\trim((string) $html)); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Build placeholder component wrapper. |
| 107 | * |
| 108 | * @param string $label |
| 109 | * The placeholder label. |
| 110 | * @param string $title |
| 111 | * (Optional) Title attribute value. |
| 112 | * @param array $vals |
| 113 | * (Optional) HTMX vals data when placeholder trigger something when moving. |
| 114 | * @param string|null $keywords |
| 115 | * (Optional) Data attributes keywords for search. |
| 116 | * |
| 117 | * @return array |
| 118 | * A renderable array. |
| 119 | */ |
| 120 | protected function buildPlaceholder(string|TranslatableMarkup $label, string $title = '', array $vals = [], ?string $keywords = NULL): array { |
| 121 | $build = [ |
| 122 | '#type' => 'component', |
| 123 | '#component' => 'display_builder:placeholder', |
| 124 | '#slots' => [ |
| 125 | 'content' => $label, |
| 126 | ], |
| 127 | ]; |
| 128 | |
| 129 | if (isset($vals['source_id'])) { |
| 130 | $build['#attributes']['class'][] = \sprintf('db-placeholder-%s', $vals['source_id']); |
| 131 | } |
| 132 | |
| 133 | $testid_source = $vals['source']['component']['component_id'] |
| 134 | ?? $vals['source']['plugin_id'] |
| 135 | ?? $vals['source']['derivable_context'] |
| 136 | ?? NULL; |
| 137 | |
| 138 | if ($testid_source !== NULL) { |
| 139 | $build['#attributes']['data-testid'] = \sprintf('placeholder-%s', \str_replace(':', '-', $testid_source)); |
| 140 | } |
| 141 | elseif (isset($vals['source_id'])) { |
| 142 | $build['#attributes']['data-testid'] = \sprintf('placeholder-%s', $vals['source_id']); |
| 143 | } |
| 144 | elseif (\is_string(\reset($vals))) { |
| 145 | $build['#attributes']['data-testid'] = \sprintf('placeholder-%s', \reset($vals)); |
| 146 | } |
| 147 | |
| 148 | if ($keywords) { |
| 149 | $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords)); |
| 150 | } |
| 151 | |
| 152 | if (!empty($title)) { |
| 153 | $build['#attributes']['title'] = $title; |
| 154 | } |
| 155 | |
| 156 | if (!empty($vals)) { |
| 157 | (new Htmx())->vals($vals)->applyTo($build); |
| 158 | } |
| 159 | |
| 160 | return $build; |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Build placeholder as Button. |
| 165 | * |
| 166 | * @param string $label |
| 167 | * The placeholder label. |
| 168 | * @param array $vals |
| 169 | * (Optional) HTMX vals data when placeholder trigger something when moving. |
| 170 | * |
| 171 | * @return array |
| 172 | * A renderable array. |
| 173 | */ |
| 174 | protected function buildPlaceholderButton(string|TranslatableMarkup $label, array $vals = []): array { |
| 175 | $build = $this->buildPlaceholder($label, '', $vals); |
| 176 | $build['#props']['variant'] = 'button'; |
| 177 | // To be able to identify the node when dragging and set the drawer title. |
| 178 | $build['#attributes']['data-node-title'] = (string) $label; |
| 179 | |
| 180 | return $build; |
| 181 | } |
| 182 | |
| 183 | /** |
| 184 | * Build placeholder as List. |
| 185 | * |
| 186 | * @param string $label |
| 187 | * The placeholder label. |
| 188 | * @param array $vals |
| 189 | * (Optional) HTMX vals data when placeholder trigger something when moving. |
| 190 | * @param string|null $keywords |
| 191 | * (Optional) Keywords attributes to add used by search. |
| 192 | * |
| 193 | * @return array |
| 194 | * A renderable array. |
| 195 | */ |
| 196 | protected function buildPlaceholderList(string|TranslatableMarkup $label, array $vals = [], ?string $keywords = NULL): array { |
| 197 | $build = $this->buildPlaceholder($label, '', $vals); |
| 198 | $build['#props']['variant'] = 'list'; |
| 199 | // To be able to identify the node when dragging and set the drawer title. |
| 200 | $build['#attributes']['data-node-title'] = (string) $label; |
| 201 | |
| 202 | if ($keywords) { |
| 203 | $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords)); |
| 204 | } |
| 205 | |
| 206 | return $build; |
| 207 | } |
| 208 | |
| 209 | /** |
| 210 | * Build placeholder. |
| 211 | * |
| 212 | * @param string $builder_id |
| 213 | * The builder id. |
| 214 | * @param string $label |
| 215 | * The placeholder label. |
| 216 | * @param array $vals |
| 217 | * HTMX vals data if the placeholder is triggering something when moving. |
| 218 | * @param \Drupal\Core\Url $preview_url |
| 219 | * The preview_url prop value. |
| 220 | * @param string|null $keywords |
| 221 | * (Optional) Keywords attributes to add used by search. |
| 222 | * |
| 223 | * @return array |
| 224 | * A renderable array. |
| 225 | */ |
| 226 | protected function buildPlaceholderListWithPreview(string $builder_id, string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL): array { |
| 227 | $build = $this->buildPlaceholderList($label, $vals, $keywords); |
| 228 | |
| 229 | // Do not include entity field previews as we don't have generated value. |
| 230 | if (isset($vals['source_id']) && ($vals['source_id'] === 'entity_field' || $vals['source_id'] === 'entity_reference')) { |
| 231 | return $build; |
| 232 | } |
| 233 | |
| 234 | $this->applyPreview($build, $builder_id, $preview_url); |
| 235 | |
| 236 | return $build; |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Build placeholder. |
| 241 | * |
| 242 | * @param string $label |
| 243 | * The placeholder label. |
| 244 | * @param array $vals |
| 245 | * HTMX vals data if the placeholder is triggering something when moving. |
| 246 | * @param string|null $keywords |
| 247 | * (Optional) Keywords attributes to add used by search. |
| 248 | * @param string|null $thumbnail |
| 249 | * (Optional) The thumbnail URL. |
| 250 | * |
| 251 | * @return array |
| 252 | * A renderable array. |
| 253 | */ |
| 254 | protected function buildPlaceholderCard(string|TranslatableMarkup $label, array $vals, ?string $keywords = NULL, ?string $thumbnail = NULL): array { |
| 255 | $build = $this->buildPlaceholder($label, '', $vals); |
| 256 | |
| 257 | if ($thumbnail) { |
| 258 | $build['#slots']['image'] = [ |
| 259 | '#type' => 'html_tag', |
| 260 | '#tag' => 'img', |
| 261 | '#attributes' => [ |
| 262 | // @todo generate proper relative url. |
| 263 | 'src' => '/' . $thumbnail, |
| 264 | ], |
| 265 | ]; |
| 266 | } |
| 267 | |
| 268 | if ($keywords) { |
| 269 | $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords)); |
| 270 | } |
| 271 | |
| 272 | return $build; |
| 273 | } |
| 274 | |
| 275 | /** |
| 276 | * Build placeholder. |
| 277 | * |
| 278 | * @param string $builder_id |
| 279 | * The builder id. |
| 280 | * @param string $label |
| 281 | * The placeholder label. |
| 282 | * @param array $vals |
| 283 | * HTMX vals data if the placeholder is triggering something when moving. |
| 284 | * @param \Drupal\Core\Url $preview_url |
| 285 | * The preview_url prop value. |
| 286 | * @param string|null $keywords |
| 287 | * (Optional) Keywords attributes to add used by search. |
| 288 | * @param string|null $thumbnail |
| 289 | * (Optional) The thumbnail URL. |
| 290 | * |
| 291 | * @return array |
| 292 | * A renderable array. |
| 293 | */ |
| 294 | protected function buildPlaceholderCardWithPreview(string $builder_id, string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL, ?string $thumbnail = NULL): array { |
| 295 | $build = $this->buildPlaceholderCard($label, $vals, $keywords, $thumbnail); |
| 296 | $this->applyPreview($build, $builder_id, $preview_url); |
| 297 | |
| 298 | return $build; |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * Build a button. |
| 303 | * |
| 304 | * Uniq id is required for keyboard mapping with ajax requests. |
| 305 | * |
| 306 | * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label |
| 307 | * The button label. |
| 308 | * @param string $action |
| 309 | * (Optional) The action value attribute. Used mainly for e2e tests. |
| 310 | * @param string|null $icon |
| 311 | * (Optional) The icon name. Default none. |
| 312 | * @param string|TranslatableMarkup|null $tooltip |
| 313 | * (Optional) Enable the tooltip feature. Default no tooltip. |
| 314 | * @param array|null $keyboard |
| 315 | * (Optional) Keyboard shortcut as associative array key => description. |
| 316 | * |
| 317 | * @return array |
| 318 | * The button render array. |
| 319 | */ |
| 320 | protected function buildButton( |
| 321 | string|TranslatableMarkup $label, |
| 322 | ?string $action, |
| 323 | ?string $icon = NULL, |
| 324 | string|TranslatableMarkup|null $tooltip = NULL, |
| 325 | ?array $keyboard = NULL, |
| 326 | ): array { |
| 327 | $button = [ |
| 328 | '#type' => 'component', |
| 329 | '#component' => 'display_builder:button', |
| 330 | '#props' => [ |
| 331 | 'label' => $label, |
| 332 | 'icon' => $icon, |
| 333 | 'tooltip' => $tooltip, |
| 334 | ], |
| 335 | ]; |
| 336 | |
| 337 | if ($keyboard) { |
| 338 | $button['#attributes']['data-keyboard-key'] = \key($keyboard); |
| 339 | $button['#attributes']['aria-keyshortcuts'] = $button['#attributes']['data-keyboard-key']; |
| 340 | $button['#attributes']['data-keyboard-help'] = \reset($keyboard) ?? ''; |
| 341 | } |
| 342 | |
| 343 | // Used to ease e2e tests. |
| 344 | if ($action) { |
| 345 | $button['#attributes']['data-island-action'] = $action; |
| 346 | $button['#attributes']['data-testid'] = $action; |
| 347 | } |
| 348 | |
| 349 | return $button; |
| 350 | } |
| 351 | |
| 352 | /** |
| 353 | * Build a menu item. |
| 354 | * |
| 355 | * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $title |
| 356 | * The menu title. |
| 357 | * @param string $value |
| 358 | * The menu value. |
| 359 | * @param string|null $icon |
| 360 | * (Optional) The icon name. Default none. |
| 361 | * @param string $icon_position |
| 362 | * (Optional) The icon position. Default 'prefix'. |
| 363 | * @param bool $disabled |
| 364 | * (Optional) Is the menu disabled? Default no. |
| 365 | * @param array $submenu |
| 366 | * (Optional) Nested menu item render arrays, e.g. built with this same |
| 367 | * method, to display as a submenu. Default none. |
| 368 | * |
| 369 | * @return array |
| 370 | * The menu item render array. |
| 371 | */ |
| 372 | protected function buildMenuItem( |
| 373 | string|TranslatableMarkup $title, |
| 374 | string $value, |
| 375 | ?string $icon = NULL, |
| 376 | string $icon_position = 'prefix', |
| 377 | bool $disabled = FALSE, |
| 378 | array $submenu = [], |
| 379 | ): array { |
| 380 | $build = [ |
| 381 | '#type' => 'component', |
| 382 | '#component' => 'display_builder:menu_item', |
| 383 | '#props' => [ |
| 384 | 'title' => $title, |
| 385 | 'value' => $value, |
| 386 | 'icon' => $icon, |
| 387 | 'icon_position' => $icon_position, |
| 388 | 'disabled' => $disabled, |
| 389 | ], |
| 390 | '#attributes' => [ |
| 391 | // Attribute data-contextual-menu is important for the js mapping. |
| 392 | // @see components/contextual_menu/contextual_menu.js |
| 393 | 'data-contextual-menu' => TRUE, |
| 394 | ], |
| 395 | ]; |
| 396 | |
| 397 | if ($submenu) { |
| 398 | $build['#slots']['submenu'] = $submenu; |
| 399 | } |
| 400 | |
| 401 | return $build; |
| 402 | } |
| 403 | |
| 404 | /** |
| 405 | * Build a menu item divider. |
| 406 | * |
| 407 | * @return array |
| 408 | * The menu item render array. |
| 409 | */ |
| 410 | protected function buildMenuDivider(): array { |
| 411 | return [ |
| 412 | '#type' => 'component', |
| 413 | '#component' => 'display_builder:menu_item', |
| 414 | '#props' => [ |
| 415 | 'variant' => 'divider', |
| 416 | ], |
| 417 | ]; |
| 418 | } |
| 419 | |
| 420 | /** |
| 421 | * Build draggables placeholders. |
| 422 | * |
| 423 | * Used in library islands. |
| 424 | * |
| 425 | * @param string $builder_id |
| 426 | * Builder ID. |
| 427 | * @param array $draggables |
| 428 | * Draggable placeholders. |
| 429 | * @param string $variant |
| 430 | * (Optional) The variant. |
| 431 | * |
| 432 | * @return array |
| 433 | * The draggables render array. |
| 434 | */ |
| 435 | protected function buildDraggables(string $builder_id, array $draggables, string $variant = ''): array { |
| 436 | $build = [ |
| 437 | '#type' => 'component', |
| 438 | '#component' => 'display_builder:draggables', |
| 439 | '#slots' => [ |
| 440 | 'content' => $draggables, |
| 441 | ], |
| 442 | '#attributes' => [ |
| 443 | // Required for JavaScript @see components/draggables/draggables.js. |
| 444 | 'data-db-id' => $builder_id, |
| 445 | ], |
| 446 | ]; |
| 447 | |
| 448 | if ($variant) { |
| 449 | $build['#props']['variant'] = $variant; |
| 450 | } |
| 451 | |
| 452 | return $build; |
| 453 | } |
| 454 | |
| 455 | /** |
| 456 | * Build tabs. |
| 457 | * |
| 458 | * @param string $id |
| 459 | * The ID. Used for saving active tab in local storage. |
| 460 | * @param array $tabs |
| 461 | * Tabs as links. |
| 462 | * @param bool $contextual |
| 463 | * (Optional) Is the tabs contextual? Default no. |
| 464 | * |
| 465 | * @return array |
| 466 | * The tabs render array. |
| 467 | */ |
| 468 | protected function buildTabs(string $id, array $tabs, bool $contextual = FALSE): array { |
| 469 | $build = [ |
| 470 | '#type' => 'component', |
| 471 | '#component' => 'display_builder:tabs', |
| 472 | '#props' => [ |
| 473 | 'tabs' => $tabs, |
| 474 | 'contextual' => $contextual, |
| 475 | ], |
| 476 | ]; |
| 477 | |
| 478 | if ($id) { |
| 479 | $build['#props']['id'] = $id; |
| 480 | } |
| 481 | |
| 482 | return $build; |
| 483 | } |
| 484 | |
| 485 | /** |
| 486 | * Build input. |
| 487 | * |
| 488 | * @param string $id |
| 489 | * The ID. Used for saving active tab in local storage. |
| 490 | * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label |
| 491 | * The label. |
| 492 | * @param string $type |
| 493 | * The input type. |
| 494 | * @param string $size |
| 495 | * (Optional) The input size. Default medium. |
| 496 | * @param string|null $autocomplete |
| 497 | * (Optional) The input autocomplete. |
| 498 | * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $placeholder |
| 499 | * (Optional) The input placeholder. |
| 500 | * @param bool|null $clearable |
| 501 | * (Optional) The input clearable. |
| 502 | * @param string|null $icon |
| 503 | * (Optional) The input icon. |
| 504 | * |
| 505 | * @return array |
| 506 | * The input render array. |
| 507 | */ |
| 508 | protected function buildInput(string $id, string|TranslatableMarkup $label, string $type, string $size = 'medium', ?string $autocomplete = NULL, string|TranslatableMarkup $placeholder = '', ?bool $clearable = NULL, ?string $icon = NULL): array { |
| 509 | $build = [ |
| 510 | '#type' => 'component', |
| 511 | '#component' => 'display_builder:input', |
| 512 | '#props' => [ |
| 513 | 'label' => $label, |
| 514 | 'variant' => $type, |
| 515 | 'size' => $size, |
| 516 | ], |
| 517 | ]; |
| 518 | |
| 519 | if ($id) { |
| 520 | $build['#props']['id'] = $id; |
| 521 | } |
| 522 | |
| 523 | if ($autocomplete) { |
| 524 | $build['#props']['autocomplete'] = $autocomplete; |
| 525 | } |
| 526 | |
| 527 | if ($placeholder) { |
| 528 | $build['#props']['placeholder'] = $placeholder; |
| 529 | } |
| 530 | |
| 531 | if ($clearable) { |
| 532 | $build['#props']['clearable'] = TRUE; |
| 533 | } |
| 534 | |
| 535 | if ($icon) { |
| 536 | $build['#props']['icon'] = $icon; |
| 537 | } |
| 538 | |
| 539 | return $build; |
| 540 | } |
| 541 | |
| 542 | /** |
| 543 | * Wraps a renderable in a div. |
| 544 | * |
| 545 | * Commonly used with tabs. |
| 546 | * |
| 547 | * @param array $content |
| 548 | * The renderable content. |
| 549 | * @param string $id |
| 550 | * (Optional) The div id. |
| 551 | * |
| 552 | * @return array |
| 553 | * The wrapped render array. |
| 554 | */ |
| 555 | protected function wrapContent(array $content, string $id = ''): array { |
| 556 | $build = [ |
| 557 | '#type' => 'html_tag', |
| 558 | '#tag' => 'div', |
| 559 | 'content' => $content, |
| 560 | ]; |
| 561 | |
| 562 | if (!empty($id)) { |
| 563 | $build['#attributes']['id'] = $id; |
| 564 | } |
| 565 | |
| 566 | return $build; |
| 567 | } |
| 568 | |
| 569 | /** |
| 570 | * Make a placeholder show a preview popup on hover. |
| 571 | * |
| 572 | * Only the request is declared here. Showing, positioning and hiding the |
| 573 | * popup belongs to js/preview.js, which can act once the response is in |
| 574 | * the DOM - htmx alone can only show an empty box the moment the pointer |
| 575 | * arrives, which Floating UI then measures at the wrong size. |
| 576 | * |
| 577 | * @param array $build |
| 578 | * The placeholder renderable, altered by reference. |
| 579 | * @param string $builder_id |
| 580 | * The builder id, owning the popup element. |
| 581 | * @param \Drupal\Core\Url $preview_url |
| 582 | * The URL returning the preview markup. |
| 583 | */ |
| 584 | private function applyPreview(array &$build, string $builder_id, Url $preview_url): void { |
| 585 | // Marks the element for js/preview.js, whatever the placeholder variant. |
| 586 | // The URL itself lives in hx-get, JS only needs to recognize a trigger. |
| 587 | $build['#attributes']['data-preview'] = TRUE; |
| 588 | |
| 589 | (new Htmx()) |
| 590 | ->get($preview_url) |
| 591 | ->target(\sprintf('#preview-%s', $builder_id)) |
| 592 | // Hover intent. js/preview.js cancels the request if the pointer left |
| 593 | // in the meantime, as htmx debounces but never cancels on its own. |
| 594 | // Placeholders are focusable, so keyboard users reach the preview the |
| 595 | // same way - focusin, because focus does not bubble to the delegated |
| 596 | // listener that arbitrates which trigger is the current one. |
| 597 | ->trigger('mouseenter delay:250ms, focusin delay:250ms') |
| 598 | ->applyTo($build); |
| 599 | } |
| 600 | |
| 601 | } |
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.
| 584 | private function applyPreview(array &$build, string $builder_id, Url $preview_url): void { |
| 585 | // Marks the element for js/preview.js, whatever the placeholder variant. |
| 586 | // The URL itself lives in hx-get, JS only needs to recognize a trigger. |
| 587 | $build['#attributes']['data-preview'] = TRUE; |
| 588 | |
| 589 | (new Htmx()) |
| 590 | ->get($preview_url) |
| 591 | ->target(\sprintf('#preview-%s', $builder_id)) |
| 592 | // Hover intent. js/preview.js cancels the request if the pointer left |
| 593 | // in the meantime, as htmx debounces but never cancels on its own. |
| 594 | // Placeholders are focusable, so keyboard users reach the preview the |
| 595 | // same way - focusin, because focus does not bubble to the delegated |
| 596 | // listener that arbitrates which trigger is the current one. |
| 597 | ->trigger('mouseenter delay:250ms, focusin delay:250ms') |
| 598 | ->applyTo($build); |
| 599 | } |
| 321 | string|TranslatableMarkup $label, |
| 322 | ?string $action, |
| 323 | ?string $icon = NULL, |
| 324 | string|TranslatableMarkup|null $tooltip = NULL, |
| 325 | ?array $keyboard = NULL, |
| 326 | ): array { |
| 327 | $button = [ |
| 328 | '#type' => 'component', |
| 329 | '#component' => 'display_builder:button', |
| 330 | '#props' => [ |
| 331 | 'label' => $label, |
| 332 | 'icon' => $icon, |
| 333 | 'tooltip' => $tooltip, |
| 334 | ], |
| 335 | ]; |
| 336 | |
| 337 | if ($keyboard) { |
| 338 | $button['#attributes']['data-keyboard-key'] = \key($keyboard); |
| 339 | $button['#attributes']['aria-keyshortcuts'] = $button['#attributes']['data-keyboard-key']; |
| 340 | $button['#attributes']['data-keyboard-help'] = \reset($keyboard) ?? ''; |
| 341 | } |
| 342 | |
| 343 | // Used to ease e2e tests. |
| 344 | if ($action) { |
| 344 | if ($action) { |
| 345 | $button['#attributes']['data-island-action'] = $action; |
| 346 | $button['#attributes']['data-testid'] = $action; |
| 347 | } |
| 348 | |
| 349 | return $button; |
| 349 | return $button; |
| 350 | } |
| 435 | protected function buildDraggables(string $builder_id, array $draggables, string $variant = ''): array { |
| 436 | $build = [ |
| 437 | '#type' => 'component', |
| 438 | '#component' => 'display_builder:draggables', |
| 439 | '#slots' => [ |
| 440 | 'content' => $draggables, |
| 441 | ], |
| 442 | '#attributes' => [ |
| 443 | // Required for JavaScript @see components/draggables/draggables.js. |
| 444 | 'data-db-id' => $builder_id, |
| 445 | ], |
| 446 | ]; |
| 447 | |
| 448 | if ($variant) { |
| 449 | $build['#props']['variant'] = $variant; |
| 450 | } |
| 451 | |
| 452 | return $build; |
| 452 | return $build; |
| 453 | } |
| 32 | public function buildError(string $builder_id, string|TranslatableMarkup $message, bool $global = FALSE, ?int $duration = NULL): array { |
| 33 | $build = [ |
| 34 | '#type' => 'component', |
| 35 | '#component' => 'display_builder:alert', |
| 36 | '#slots' => [ |
| 37 | 'content' => $message, |
| 38 | ], |
| 39 | '#props' => [ |
| 40 | 'variant' => 'danger', |
| 41 | 'icon' => 'exclamation-octagon', |
| 42 | 'open' => TRUE, |
| 43 | 'closable' => TRUE, |
| 44 | ], |
| 45 | '#attributes' => [ |
| 46 | 'class' => 'db-message', |
| 47 | ], |
| 48 | ]; |
| 49 | |
| 50 | if ($duration) { |
| 51 | $build['#props']['duration'] = $duration; |
| 52 | } |
| 53 | |
| 54 | if ($global) { |
| 54 | if ($global) { |
| 61 | '#type' => 'container', |
| 62 | 'message' => $build, |
| 63 | ]; |
| 64 | (new Htmx()) |
| 65 | ->swapOob(\sprintf('beforeend:#message-%s', $builder_id)) |
| 66 | ->applyTo($build); |
| 67 | } |
| 68 | |
| 69 | return $build; |
| 69 | return $build; |
| 70 | } |
| 508 | protected function buildInput(string $id, string|TranslatableMarkup $label, string $type, string $size = 'medium', ?string $autocomplete = NULL, string|TranslatableMarkup $placeholder = '', ?bool $clearable = NULL, ?string $icon = NULL): array { |
| 509 | $build = [ |
| 510 | '#type' => 'component', |
| 511 | '#component' => 'display_builder:input', |
| 512 | '#props' => [ |
| 513 | 'label' => $label, |
| 514 | 'variant' => $type, |
| 515 | 'size' => $size, |
| 516 | ], |
| 517 | ]; |
| 518 | |
| 519 | if ($id) { |
| 520 | $build['#props']['id'] = $id; |
| 521 | } |
| 522 | |
| 523 | if ($autocomplete) { |
| 523 | if ($autocomplete) { |
| 524 | $build['#props']['autocomplete'] = $autocomplete; |
| 525 | } |
| 526 | |
| 527 | if ($placeholder) { |
| 527 | if ($placeholder) { |
| 528 | $build['#props']['placeholder'] = $placeholder; |
| 529 | } |
| 530 | |
| 531 | if ($clearable) { |
| 531 | if ($clearable) { |
| 532 | $build['#props']['clearable'] = TRUE; |
| 533 | } |
| 534 | |
| 535 | if ($icon) { |
| 535 | if ($icon) { |
| 536 | $build['#props']['icon'] = $icon; |
| 537 | } |
| 538 | |
| 539 | return $build; |
| 539 | return $build; |
| 540 | } |
| 412 | '#type' => 'component', |
| 413 | '#component' => 'display_builder:menu_item', |
| 414 | '#props' => [ |
| 415 | 'variant' => 'divider', |
| 416 | ], |
| 417 | ]; |
| 418 | } |
| 373 | string|TranslatableMarkup $title, |
| 374 | string $value, |
| 375 | ?string $icon = NULL, |
| 376 | string $icon_position = 'prefix', |
| 377 | bool $disabled = FALSE, |
| 378 | array $submenu = [], |
| 379 | ): array { |
| 380 | $build = [ |
| 381 | '#type' => 'component', |
| 382 | '#component' => 'display_builder:menu_item', |
| 383 | '#props' => [ |
| 384 | 'title' => $title, |
| 385 | 'value' => $value, |
| 386 | 'icon' => $icon, |
| 387 | 'icon_position' => $icon_position, |
| 388 | 'disabled' => $disabled, |
| 389 | ], |
| 390 | '#attributes' => [ |
| 391 | // Attribute data-contextual-menu is important for the js mapping. |
| 392 | // @see components/contextual_menu/contextual_menu.js |
| 393 | 'data-contextual-menu' => TRUE, |
| 394 | ], |
| 395 | ]; |
| 396 | |
| 397 | if ($submenu) { |
| 398 | $build['#slots']['submenu'] = $submenu; |
| 399 | } |
| 400 | |
| 401 | return $build; |
| 401 | return $build; |
| 402 | } |
| 120 | protected function buildPlaceholder(string|TranslatableMarkup $label, string $title = '', array $vals = [], ?string $keywords = NULL): array { |
| 121 | $build = [ |
| 122 | '#type' => 'component', |
| 123 | '#component' => 'display_builder:placeholder', |
| 124 | '#slots' => [ |
| 125 | 'content' => $label, |
| 126 | ], |
| 127 | ]; |
| 128 | |
| 129 | if (isset($vals['source_id'])) { |
| 130 | $build['#attributes']['class'][] = \sprintf('db-placeholder-%s', $vals['source_id']); |
| 131 | } |
| 132 | |
| 133 | $testid_source = $vals['source']['component']['component_id'] |
| 133 | $testid_source = $vals['source']['component']['component_id'] |
| 134 | ?? $vals['source']['plugin_id'] |
| 135 | ?? $vals['source']['derivable_context'] |
| 136 | ?? NULL; |
| 137 | |
| 138 | if ($testid_source !== NULL) { |
| 138 | if ($testid_source !== NULL) { |
| 139 | $build['#attributes']['data-testid'] = \sprintf('placeholder-%s', \str_replace(':', '-', $testid_source)); |
| 141 | elseif (isset($vals['source_id'])) { |
| 141 | elseif (isset($vals['source_id'])) { |
| 142 | $build['#attributes']['data-testid'] = \sprintf('placeholder-%s', $vals['source_id']); |
| 144 | elseif (\is_string(\reset($vals))) { |
| 145 | $build['#attributes']['data-testid'] = \sprintf('placeholder-%s', \reset($vals)); |
| 146 | } |
| 147 | |
| 148 | if ($keywords) { |
| 148 | if ($keywords) { |
| 149 | $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords)); |
| 150 | } |
| 151 | |
| 152 | if (!empty($title)) { |
| 152 | if (!empty($title)) { |
| 153 | $build['#attributes']['title'] = $title; |
| 154 | } |
| 155 | |
| 156 | if (!empty($vals)) { |
| 156 | if (!empty($vals)) { |
| 157 | (new Htmx())->vals($vals)->applyTo($build); |
| 158 | } |
| 159 | |
| 160 | return $build; |
| 160 | return $build; |
| 161 | } |
| 174 | protected function buildPlaceholderButton(string|TranslatableMarkup $label, array $vals = []): array { |
| 175 | $build = $this->buildPlaceholder($label, '', $vals); |
| 176 | $build['#props']['variant'] = 'button'; |
| 177 | // To be able to identify the node when dragging and set the drawer title. |
| 178 | $build['#attributes']['data-node-title'] = (string) $label; |
| 179 | |
| 180 | return $build; |
| 181 | } |
| 254 | protected function buildPlaceholderCard(string|TranslatableMarkup $label, array $vals, ?string $keywords = NULL, ?string $thumbnail = NULL): array { |
| 255 | $build = $this->buildPlaceholder($label, '', $vals); |
| 256 | |
| 257 | if ($thumbnail) { |
| 259 | '#type' => 'html_tag', |
| 260 | '#tag' => 'img', |
| 261 | '#attributes' => [ |
| 262 | // @todo generate proper relative url. |
| 263 | 'src' => '/' . $thumbnail, |
| 264 | ], |
| 265 | ]; |
| 266 | } |
| 267 | |
| 268 | if ($keywords) { |
| 268 | if ($keywords) { |
| 269 | $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords)); |
| 270 | } |
| 271 | |
| 272 | return $build; |
| 272 | return $build; |
| 273 | } |
| 294 | protected function buildPlaceholderCardWithPreview(string $builder_id, string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL, ?string $thumbnail = NULL): array { |
| 295 | $build = $this->buildPlaceholderCard($label, $vals, $keywords, $thumbnail); |
| 296 | $this->applyPreview($build, $builder_id, $preview_url); |
| 297 | |
| 298 | return $build; |
| 299 | } |
| 196 | protected function buildPlaceholderList(string|TranslatableMarkup $label, array $vals = [], ?string $keywords = NULL): array { |
| 197 | $build = $this->buildPlaceholder($label, '', $vals); |
| 198 | $build['#props']['variant'] = 'list'; |
| 199 | // To be able to identify the node when dragging and set the drawer title. |
| 200 | $build['#attributes']['data-node-title'] = (string) $label; |
| 201 | |
| 202 | if ($keywords) { |
| 203 | $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords)); |
| 204 | } |
| 205 | |
| 206 | return $build; |
| 206 | return $build; |
| 207 | } |
| 226 | protected function buildPlaceholderListWithPreview(string $builder_id, string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL): array { |
| 227 | $build = $this->buildPlaceholderList($label, $vals, $keywords); |
| 228 | |
| 229 | // Do not include entity field previews as we don't have generated value. |
| 230 | if (isset($vals['source_id']) && ($vals['source_id'] === 'entity_field' || $vals['source_id'] === 'entity_reference')) { |
| 230 | if (isset($vals['source_id']) && ($vals['source_id'] === 'entity_field' || $vals['source_id'] === 'entity_reference')) { |
| 230 | if (isset($vals['source_id']) && ($vals['source_id'] === 'entity_field' || $vals['source_id'] === 'entity_reference')) { |
| 230 | if (isset($vals['source_id']) && ($vals['source_id'] === 'entity_field' || $vals['source_id'] === 'entity_reference')) { |
| 230 | if (isset($vals['source_id']) && ($vals['source_id'] === 'entity_field' || $vals['source_id'] === 'entity_reference')) { |
| 231 | return $build; |
| 234 | $this->applyPreview($build, $builder_id, $preview_url); |
| 235 | |
| 236 | return $build; |
| 237 | } |
| 468 | protected function buildTabs(string $id, array $tabs, bool $contextual = FALSE): array { |
| 469 | $build = [ |
| 470 | '#type' => 'component', |
| 471 | '#component' => 'display_builder:tabs', |
| 472 | '#props' => [ |
| 473 | 'tabs' => $tabs, |
| 474 | 'contextual' => $contextual, |
| 475 | ], |
| 476 | ]; |
| 477 | |
| 478 | if ($id) { |
| 479 | $build['#props']['id'] = $id; |
| 480 | } |
| 481 | |
| 482 | return $build; |
| 482 | return $build; |
| 483 | } |
| 94 | protected function isRenderEmptyOrFailing(RendererInterface $renderer, array $renderable): bool { |
| 95 | try { |
| 96 | $html = $renderer->renderInIsolation($renderable); |
| 98 | catch (\Throwable $e) { |
| 99 | return TRUE; |
| 102 | return empty(\trim((string) $html)); |
| 103 | } |
| 555 | protected function wrapContent(array $content, string $id = ''): array { |
| 556 | $build = [ |
| 557 | '#type' => 'html_tag', |
| 558 | '#tag' => 'div', |
| 559 | 'content' => $content, |
| 560 | ]; |
| 561 | |
| 562 | if (!empty($id)) { |
| 563 | $build['#attributes']['id'] = $id; |
| 564 | } |
| 565 | |
| 566 | return $build; |
| 566 | return $build; |
| 567 | } |