Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
23.67% covered (danger)
23.67%
40 / 169
24.53% covered (danger)
24.53%
13 / 53
3.95% covered (danger)
3.95%
3 / 76
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
RenderableBuilderTrait
23.67% covered (danger)
23.67%
40 / 169
24.53% covered (danger)
24.53%
13 / 53
3.95% covered (danger)
3.95%
3 / 76
0.00% covered (danger)
0.00%
0 / 13
998.06
0.00% covered (danger)
0.00%
0 / 1
 buildError
95.83% covered (success)
95.83%
23 / 24
85.71% covered (warning)
85.71%
6 / 7
12.50% covered (danger)
12.50%
1 / 8
0.00% covered (danger)
0.00%
0 / 1
14.72
 buildPlaceholder
75.00% covered (warning)
75.00%
12 / 16
55.56% covered (warning)
55.56%
5 / 9
6.25% covered (danger)
6.25%
1 / 16
0.00% covered (danger)
0.00%
0 / 1
25.60
 buildPlaceholderButton
83.33% covered (warning)
83.33%
5 / 6
66.67% covered (warning)
66.67%
2 / 3
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 buildPlaceholderButtonWithPreview
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildPlaceholderCardWithPreview
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildButton
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildIconButton
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildMenuItem
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildMenuDivider
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildDraggables
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 buildTabs
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 buildInput
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
42
 wrapContent
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder;
6
7use Drupal\Core\StringTranslation\TranslatableMarkup;
8use Drupal\Core\Url;
9
10/**
11 * Trait with helpers to build renderables.
12 */
13trait RenderableBuilderTrait {
14
15  /**
16   * Build error message.
17   *
18   * @param string $builder_id
19   *   The builder id.
20   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $message
21   *   The message to display.
22   * @param string|null $debug
23   *   (Optional) Debug information to print.
24   * @param int|null $duration
25   *   (Optional) Alert duration before closing.
26   * @param bool $global
27   *   (Optional) Try to use the default builder message placeholder.
28   *
29   * @return array
30   *   The input render array.
31   */
32  public function buildError(string $builder_id, string|TranslatableMarkup $message, ?string $debug = NULL, ?int $duration = NULL, bool $global = FALSE): 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 ($debug) {
51      $build['#slots']['debug'] = $debug;
52    }
53
54    if ($duration) {
55      $build['#props']['duration'] = $duration;
56    }
57
58    if ($global) {
59      $build['#props']['id'] = \sprintf('message-%s', $builder_id);
60      $build['#attributes']['hx-swap-oob'] = 'true';
61    }
62
63    return $build;
64  }
65
66  /**
67   * Build placeholder.
68   *
69   * @param string $label
70   *   The placeholder label.
71   * @param string $title
72   *   (Optional) Title attribute value.
73   * @param array $vals
74   *   (Optional) HTMX vals data when placeholder trigger something when moving.
75   * @param string|null $keywords
76   *   (Optional) Data attributes keywords for search.
77   *
78   * @return array
79   *   A renderable array.
80   */
81  protected function buildPlaceholder(string|TranslatableMarkup $label, string $title = '', array $vals = [], ?string $keywords = NULL): array {
82    $build = [
83      '#type' => 'component',
84      '#component' => 'display_builder:placeholder',
85      '#slots' => [
86        'content' => $label,
87      ],
88    ];
89
90    if (isset($vals['source_id'])) {
91      $build['#attributes']['class'][] = \sprintf('db-placeholder-%s', $vals['source_id']);
92    }
93
94    if ($keywords) {
95      $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords));
96    }
97
98    if (!empty($title)) {
99      $build['#attributes']['title'] = $title;
100    }
101
102    if (!empty($vals)) {
103      $build['#attributes']['hx-vals'] = \json_encode($vals);
104    }
105
106    return $build;
107  }
108
109  /**
110   * Build placeholder.
111   *
112   * @param string $label
113   *   The placeholder label.
114   * @param array $vals
115   *   (Optional) HTMX vals data when placeholder trigger something when moving.
116   * @param string|null $keywords
117   *   (Optional) Keywords attributes to add used by search.
118   *
119   * @return array
120   *   A renderable array.
121   */
122  protected function buildPlaceholderButton(string|TranslatableMarkup $label, array $vals = [], ?string $keywords = NULL): array {
123    $build = $this->buildPlaceholder($label, '', $vals);
124    $build['#props']['variant'] = 'button';
125    // To be able to identify the node when dragging and set the drawer title.
126    $build['#attributes']['data-node-title'] = (string) $label;
127
128    if ($keywords) {
129      $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords));
130    }
131
132    return $build;
133  }
134
135  /**
136   * Build placeholder.
137   *
138   * @param string $builder_id
139   *   The builder id.
140   * @param string $label
141   *   The placeholder label.
142   * @param array $vals
143   *   HTMX vals data if the placeholder is triggering something when moving.
144   * @param \Drupal\Core\Url $preview_url
145   *   The preview_url prop value.
146   * @param string|null $keywords
147   *   (Optional) Keywords attributes to add used by search.
148   *
149   * @return array
150   *   A renderable array.
151   */
152  protected function buildPlaceholderButtonWithPreview(string $builder_id, string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL): array {
153    $build = $this->buildPlaceholderButton($label, $vals, $keywords);
154
155    $hide_script = \sprintf('Drupal.displayBuilder.hidePreview(%s)', $builder_id);
156    $attributes = [
157      'hx-get' => $preview_url->toString(),
158      'hx-target' => \sprintf('#preview-%s', $builder_id),
159      'hx-trigger' => 'mouseenter delay:250ms',
160      'hx-on:mouseenter' => \sprintf('Drupal.displayBuilder.showPreview(%s, this)', $builder_id),
161      'hx-on:focus' => \sprintf('Drupal.displayBuilder.showPreview(%s, this)', $builder_id),
162      'hx-on:mouseleave' => $hide_script,
163      'hx-on:blur' => $hide_script,
164      // Disable the preview on click for a dragging operation.
165      'hx-on:mousedown' => $hide_script,
166    ];
167
168    $build['#attributes'] = \array_merge($build['#attributes'], $attributes);
169
170    return $build;
171  }
172
173  /**
174   * Build placeholder.
175   *
176   * @param string $label
177   *   The placeholder label.
178   * @param array $vals
179   *   HTMX vals data if the placeholder is triggering something when moving.
180   * @param \Drupal\Core\Url $preview_url
181   *   The preview_url prop value.
182   * @param string|null $keywords
183   *   (Optional) Keywords attributes to add used by search.
184   * @param string|null $thumbnail
185   *   (Optional) The thumbnail URL.
186   *
187   * @return array
188   *   A renderable array.
189   */
190  protected function buildPlaceholderCardWithPreview(string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL, ?string $thumbnail = NULL): array {
191    $build = $this->buildPlaceholder($label, '', $vals);
192    $build['#props']['preview_url'] = $preview_url;
193
194    if ($thumbnail) {
195      $build['#slots']['image'] = [
196        '#type' => 'html_tag',
197        '#tag' => 'img',
198        '#attributes' => [
199          // @todo generate proper relative url.
200          'src' => '/' . $thumbnail,
201        ],
202      ];
203    }
204
205    if ($keywords) {
206      $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords));
207    }
208
209    return $build;
210  }
211
212  /**
213   * Build a button.
214   *
215   * Uniq id is required for keyboard mapping with ajax requests.
216   *
217   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
218   *   The button label.
219   * @param string $action
220   *   (Optional) The action value attribute. Used mainly for e2e tests.
221   * @param string|null $icon
222   *   (Optional) The icon name. Default none.
223   * @param string|TranslatableMarkup|null $tooltip
224   *   (Optional) Enable the tooltip feature. Default no tooltip.
225   * @param array|null $keyboard
226   *   (Optional) Keyboard shortcut as associative array key => description.
227   *
228   * @return array
229   *   The button render array.
230   */
231  protected function buildButton(
232    string|TranslatableMarkup $label,
233    ?string $action,
234    ?string $icon = NULL,
235    string|TranslatableMarkup|null $tooltip = NULL,
236    ?array $keyboard = NULL,
237  ): array {
238    $button = [
239      '#type' => 'component',
240      '#component' => 'display_builder:button',
241      '#props' => [
242        'label' => $label,
243        'icon' => $icon,
244        'tooltip' => $tooltip,
245      ],
246    ];
247
248    if ($keyboard) {
249      $button['#attributes']['data-keyboard-key'] = \key($keyboard);
250      $button['#attributes']['data-keyboard-help'] = \reset($keyboard) ?? '';
251    }
252
253    // Used to ease e2e tests.
254    if ($action) {
255      $button['#attributes']['data-island-action'] = $action;
256    }
257
258    return $button;
259  }
260
261  /**
262   * Build an icon button.
263   *
264   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
265   *   The button label.
266   * @param string|null $icon
267   *   (Optional) The icon name. Default none.
268   *
269   * @return array
270   *   The icon button render array.
271   *
272   * @todo never used, replace existing buildButton() where relevant
273   */
274  protected function buildIconButton(string|TranslatableMarkup $label, ?string $icon = NULL): array {
275    return [
276      '#type' => 'component',
277      '#component' => 'display_builder:icon_button',
278      '#props' => [
279        'icon' => $icon ?? '',
280        'label' => $label,
281      ],
282    ];
283  }
284
285  /**
286   * Build a menu item.
287   *
288   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $title
289   *   The menu title.
290   * @param string $value
291   *   The menu value.
292   * @param string|null $icon
293   *   (Optional) The icon name. Default none.
294   * @param string $icon_position
295   *   (Optional) The icon position. Default 'prefix'.
296   * @param bool $disabled
297   *   (Optional) Is the menu disabled? Default no.
298   *
299   * @return array
300   *   The menu item render array.
301   */
302  protected function buildMenuItem(
303    string|TranslatableMarkup $title,
304    string $value,
305    ?string $icon = NULL,
306    string $icon_position = 'prefix',
307    bool $disabled = FALSE,
308  ): array {
309    return [
310      '#type' => 'component',
311      '#component' => 'display_builder:menu_item',
312      '#props' => [
313        'title' => $title,
314        'value' => $value,
315        'icon' => $icon,
316        'icon_position' => $icon_position,
317        'disabled' => $disabled,
318      ],
319      '#attributes' => [
320        'data-contextual-menu' => TRUE,
321      ],
322    ];
323  }
324
325  /**
326   * Build a menu item divider.
327   *
328   * @return array
329   *   The menu item render array.
330   */
331  protected function buildMenuDivider(): array {
332    return [
333      '#type' => 'component',
334      '#component' => 'display_builder:menu_item',
335      '#props' => [
336        'variant' => 'divider',
337      ],
338    ];
339  }
340
341  /**
342   * Build draggables placeholders.
343   *
344   * Used in library islands.
345   *
346   * @param string $builder_id
347   *   Builder ID.
348   * @param array $draggables
349   *   Draggable placeholders.
350   * @param string $variant
351   *   (Optional) The variant.
352   *
353   * @return array
354   *   The draggables render array.
355   */
356  protected function buildDraggables(string $builder_id, array $draggables, string $variant = ''): array {
357    $build = [
358      '#type' => 'component',
359      '#component' => 'display_builder:draggables',
360      '#slots' => [
361        'content' => $draggables,
362      ],
363      '#attributes' => [
364        // Required for JavaScript @see components/draggables/draggables.js.
365        'data-db-id' => $builder_id,
366      ],
367    ];
368
369    if ($variant) {
370      $build['#props']['variant'] = $variant;
371    }
372
373    return $build;
374  }
375
376  /**
377   * Build tabs.
378   *
379   * @param string $id
380   *   The ID. Used for saving active tab in local storage.
381   * @param array $tabs
382   *   Tabs as links.
383   * @param bool $contextual
384   *   (Optional) Is the tabs contextual? Default no.
385   *
386   * @return array
387   *   The tabs render array.
388   */
389  protected function buildTabs(string $id, array $tabs, bool $contextual = FALSE): array {
390    $build = [
391      '#type' => 'component',
392      '#component' => 'display_builder:tabs',
393      '#props' => [
394        'tabs' => $tabs,
395        'contextual' => $contextual,
396      ],
397    ];
398
399    if ($id) {
400      $build['#props']['id'] = $id;
401    }
402
403    return $build;
404  }
405
406  /**
407   * Build input.
408   *
409   * @param string $id
410   *   The ID. Used for saving active tab in local storage.
411   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
412   *   The label.
413   * @param string $type
414   *   The input type.
415   * @param string $size
416   *   (Optional) The input size. Default medium.
417   * @param string|null $autocomplete
418   *   (Optional) The input autocomplete.
419   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $placeholder
420   *   (Optional) The input placeholder.
421   * @param bool|null $clearable
422   *   (Optional) The input clearable.
423   * @param string|null $icon
424   *   (Optional) The input icon.
425   *
426   * @return array
427   *   The input render array.
428   */
429  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 {
430    $build = [
431      '#type' => 'component',
432      '#component' => 'display_builder:input',
433      '#props' => [
434        'label' => $label,
435        'variant' => $type,
436        'size' => $size,
437      ],
438    ];
439
440    if ($id) {
441      $build['#props']['id'] = $id;
442    }
443
444    if ($autocomplete) {
445      $build['#props']['autocomplete'] = $autocomplete;
446    }
447
448    if ($placeholder) {
449      $build['#props']['placeholder'] = $placeholder;
450    }
451
452    if ($clearable) {
453      $build['#props']['clearable'] = TRUE;
454    }
455
456    if ($icon) {
457      $build['#props']['icon'] = $icon;
458    }
459
460    return $build;
461  }
462
463  /**
464   * Wraps a renderable in a div.
465   *
466   * Commonly used with tabs.
467   *
468   * @param array $content
469   *   The renderable content.
470   * @param string $id
471   *   (Optional) The div id.
472   *
473   * @return array
474   *   The wrapped render array.
475   */
476  protected function wrapContent(array $content, string $id = ''): array {
477    $build = [
478      '#type' => 'html_tag',
479      '#tag' => 'div',
480      'content' => $content,
481    ];
482
483    if (!empty($id)) {
484      $build['#attributes']['id'] = $id;
485    }
486
487    return $build;
488  }
489
490}