Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
191 / 191
100.00% covered (success)
100.00%
71 / 71
26.92% covered (danger)
26.92%
35 / 130
100.00% covered (success)
100.00%
16 / 16
CRAP
100.00% covered (success)
100.00%
1 / 1
RenderableBuilderTrait
100.00% covered (success)
100.00%
191 / 191
100.00% covered (success)
100.00%
71 / 71
26.92% covered (danger)
26.92%
35 / 130
100.00% covered (success)
100.00%
16 / 16
764.57
100.00% covered (success)
100.00%
1 / 1
 buildError
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 isRenderEmptyOrFailing
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildPlaceholder
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
15 / 15
7.81% covered (danger)
7.81%
5 / 64
100.00% covered (success)
100.00%
1 / 1
58.14
 buildPlaceholderButton
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildPlaceholderList
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildPlaceholderListWithPreview
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
7 / 7
50.00% covered (danger)
50.00%
3 / 6
100.00% covered (success)
100.00%
1 / 1
6.00
 buildPlaceholderCard
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 buildPlaceholderCardWithPreview
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildButton
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 buildMenuItem
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildMenuDivider
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 buildDraggables
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildTabs
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildInput
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
11 / 11
6.25% covered (danger)
6.25%
2 / 32
100.00% covered (success)
100.00%
1 / 1
35.66
 wrapContent
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 applyPreview
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder;
6
7use Drupal\Core\Htmx\Htmx;
8use Drupal\Core\Render\RendererInterface;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\Core\Url;
11
12/**
13 * Trait with helpers to build renderables.
14 */
15trait 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}

Branches

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.

RenderableBuilderTrait->applyPreview
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  }
RenderableBuilderTrait->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) {
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  }
RenderableBuilderTrait->buildDraggables
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  }
RenderableBuilderTrait->buildError
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  }
RenderableBuilderTrait->buildInput
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  }
RenderableBuilderTrait->buildMenuDivider
412      '#type' => 'component',
413      '#component' => 'display_builder:menu_item',
414      '#props' => [
415        'variant' => 'divider',
416      ],
417    ];
418  }
RenderableBuilderTrait->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;
401    return $build;
402  }
RenderableBuilderTrait->buildPlaceholder
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  }
RenderableBuilderTrait->buildPlaceholderButton
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  }
RenderableBuilderTrait->buildPlaceholderCard
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  }
RenderableBuilderTrait->buildPlaceholderCardWithPreview
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  }
RenderableBuilderTrait->buildPlaceholderList
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  }
RenderableBuilderTrait->buildPlaceholderListWithPreview
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  }
RenderableBuilderTrait->buildTabs
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  }
RenderableBuilderTrait->isRenderEmptyOrFailing
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  }
RenderableBuilderTrait->wrapContent
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  }