Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
40.59% covered (danger)
40.59%
69 / 170
36.84% covered (danger)
36.84%
21 / 57
10.39% covered (danger)
10.39%
8 / 77
15.38% covered (danger)
15.38%
2 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
RenderableBuilderTrait
40.59% covered (danger)
40.59%
69 / 170
36.84% covered (danger)
36.84%
21 / 57
10.39% covered (danger)
10.39%
8 / 77
15.38% covered (danger)
15.38%
2 / 13
916.48
0.00% covered (danger)
0.00%
0 / 1
 buildError
0.00% covered (danger)
0.00%
0 / 22
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
 buildPlaceholder
81.25% covered (warning)
81.25%
13 / 16
66.67% covered (warning)
66.67%
6 / 9
12.50% covered (danger)
12.50%
2 / 16
0.00% covered (danger)
0.00%
0 / 1
21.75
 buildPlaceholderButton
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
 buildPlaceholderButtonWithPreview
93.75% covered (success)
93.75%
15 / 16
42.86% covered (danger)
42.86%
3 / 7
16.67% covered (danger)
16.67%
1 / 6
0.00% covered (danger)
0.00%
0 / 1
13.26
 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
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
5 / 5
25.00% covered (danger)
25.00%
1 / 4
100.00% covered (success)
100.00%
1 / 1
6.80
 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
92.31% covered (success)
92.31%
12 / 13
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
 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
87.50% covered (warning)
87.50%
7 / 8
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
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 bool $global
23   *   (Optional) Try to use the default builder message placeholder.
24   * @param int|null $duration
25   *   (Optional) Alert duration before closing.
26   *
27   * @return array
28   *   The input render array.
29   */
30  public function buildError(string $builder_id, string|TranslatableMarkup $message, bool $global = FALSE, ?int $duration = NULL): array {
31    $build = [
32      '#type' => 'component',
33      '#component' => 'display_builder:alert',
34      '#slots' => [
35        'content' => $message,
36      ],
37      '#props' => [
38        'variant' => 'danger',
39        'icon' => 'exclamation-octagon',
40        'open' => TRUE,
41        'closable' => TRUE,
42      ],
43      '#attributes' => [
44        'class' => 'db-message',
45      ],
46    ];
47
48    if ($duration) {
49      $build['#props']['duration'] = $duration;
50    }
51
52    if ($global) {
53      $build['#props']['id'] = \sprintf('message-%s', $builder_id);
54      $build['#attributes']['hx-swap-oob'] = 'true';
55    }
56
57    return $build;
58  }
59
60  /**
61   * Build placeholder.
62   *
63   * @param string $label
64   *   The placeholder label.
65   * @param string $title
66   *   (Optional) Title attribute value.
67   * @param array $vals
68   *   (Optional) HTMX vals data when placeholder trigger something when moving.
69   * @param string|null $keywords
70   *   (Optional) Data attributes keywords for search.
71   *
72   * @return array
73   *   A renderable array.
74   */
75  protected function buildPlaceholder(string|TranslatableMarkup $label, string $title = '', array $vals = [], ?string $keywords = NULL): array {
76    $build = [
77      '#type' => 'component',
78      '#component' => 'display_builder:placeholder',
79      '#slots' => [
80        'content' => $label,
81      ],
82    ];
83
84    if (isset($vals['source_id'])) {
85      $build['#attributes']['class'][] = \sprintf('db-placeholder-%s', $vals['source_id']);
86    }
87
88    if ($keywords) {
89      $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords));
90    }
91
92    if (!empty($title)) {
93      $build['#attributes']['title'] = $title;
94    }
95
96    if (!empty($vals)) {
97      $build['#attributes']['hx-vals'] = \json_encode($vals);
98    }
99
100    return $build;
101  }
102
103  /**
104   * Build placeholder.
105   *
106   * @param string $label
107   *   The placeholder label.
108   * @param array $vals
109   *   (Optional) HTMX vals data when placeholder trigger something when moving.
110   * @param string|null $keywords
111   *   (Optional) Keywords attributes to add used by search.
112   *
113   * @return array
114   *   A renderable array.
115   */
116  protected function buildPlaceholderButton(string|TranslatableMarkup $label, array $vals = [], ?string $keywords = NULL): array {
117    $build = $this->buildPlaceholder($label, '', $vals);
118    $build['#props']['variant'] = 'button';
119    // To be able to identify the node when dragging and set the drawer title.
120    $build['#attributes']['data-node-title'] = (string) $label;
121
122    if ($keywords) {
123      $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords));
124    }
125
126    return $build;
127  }
128
129  /**
130   * Build placeholder.
131   *
132   * @param string $builder_id
133   *   The builder id.
134   * @param string $label
135   *   The placeholder label.
136   * @param array $vals
137   *   HTMX vals data if the placeholder is triggering something when moving.
138   * @param \Drupal\Core\Url $preview_url
139   *   The preview_url prop value.
140   * @param string|null $keywords
141   *   (Optional) Keywords attributes to add used by search.
142   *
143   * @return array
144   *   A renderable array.
145   */
146  protected function buildPlaceholderButtonWithPreview(string $builder_id, string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL): array {
147    $build = $this->buildPlaceholderButton($label, $vals, $keywords);
148
149    // Do not include entity field previews as we don't have generated value.
150    if (isset($vals['source_id']) && ($vals['source_id'] === 'entity_field' || $vals['source_id'] === 'entity_reference')) {
151      return $build;
152    }
153
154    $hide_script = \sprintf('Drupal.displayBuilder.hidePreview(%s)', $builder_id);
155    $attributes = [
156      'hx-get' => $preview_url->toString(),
157      'hx-target' => \sprintf('#preview-%s', $builder_id),
158      'hx-trigger' => 'mouseenter delay:250ms',
159      'hx-on:mouseenter' => \sprintf('Drupal.displayBuilder.showPreview(%s, this)', $builder_id),
160      'hx-on:focus' => \sprintf('Drupal.displayBuilder.showPreview(%s, this)', $builder_id),
161      'hx-on:mouseleave' => $hide_script,
162      'hx-on:blur' => $hide_script,
163      // Disable the preview on click for a dragging operation.
164      'hx-on:mousedown' => $hide_script,
165    ];
166
167    $build['#attributes'] = \array_merge($build['#attributes'], $attributes);
168
169    return $build;
170  }
171
172  /**
173   * Build placeholder.
174   *
175   * @param string $label
176   *   The placeholder label.
177   * @param array $vals
178   *   HTMX vals data if the placeholder is triggering something when moving.
179   * @param \Drupal\Core\Url $preview_url
180   *   The preview_url prop value.
181   * @param string|null $keywords
182   *   (Optional) Keywords attributes to add used by search.
183   * @param string|null $thumbnail
184   *   (Optional) The thumbnail URL.
185   *
186   * @return array
187   *   A renderable array.
188   */
189  protected function buildPlaceholderCardWithPreview(string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL, ?string $thumbnail = NULL): array {
190    $build = $this->buildPlaceholder($label, '', $vals);
191    $build['#props']['preview_url'] = $preview_url;
192
193    if ($thumbnail) {
194      $build['#slots']['image'] = [
195        '#type' => 'html_tag',
196        '#tag' => 'img',
197        '#attributes' => [
198          // @todo generate proper relative url.
199          'src' => '/' . $thumbnail,
200        ],
201      ];
202    }
203
204    if ($keywords) {
205      $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords));
206    }
207
208    return $build;
209  }
210
211  /**
212   * Build a button.
213   *
214   * Uniq id is required for keyboard mapping with ajax requests.
215   *
216   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
217   *   The button label.
218   * @param string $action
219   *   (Optional) The action value attribute. Used mainly for e2e tests.
220   * @param string|null $icon
221   *   (Optional) The icon name. Default none.
222   * @param string|TranslatableMarkup|null $tooltip
223   *   (Optional) Enable the tooltip feature. Default no tooltip.
224   * @param array|null $keyboard
225   *   (Optional) Keyboard shortcut as associative array key => description.
226   *
227   * @return array
228   *   The button render array.
229   */
230  protected function buildButton(
231    string|TranslatableMarkup $label,
232    ?string $action,
233    ?string $icon = NULL,
234    string|TranslatableMarkup|null $tooltip = NULL,
235    ?array $keyboard = NULL,
236  ): array {
237    $button = [
238      '#type' => 'component',
239      '#component' => 'display_builder:button',
240      '#props' => [
241        'label' => $label,
242        'icon' => $icon,
243        'tooltip' => $tooltip,
244      ],
245    ];
246
247    if ($keyboard) {
248      $button['#attributes']['data-keyboard-key'] = \key($keyboard);
249      $button['#attributes']['aria-keyshortcuts'] = $button['#attributes']['data-keyboard-key'];
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}