Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
13.86% covered (danger)
13.86%
23 / 166
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
RenderableBuilderTrait
13.86% covered (danger)
13.86%
23 / 166
0.00% covered (danger)
0.00%
0 / 13
729.16
0.00% covered (danger)
0.00%
0 / 1
 buildError
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
4
 buildPlaceholder
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 buildPlaceholderButton
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 buildPlaceholderButtonWithPreview
0.00% covered (danger)
0.00%
0 / 12
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 / 1
12
 buildButton
0.00% covered (danger)
0.00%
0 / 15
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
2
 buildMenuItem
0.00% covered (danger)
0.00%
0 / 14
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
2
 buildDraggables
0.00% covered (danger)
0.00%
0 / 13
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 / 1
6
 buildInput
0.00% covered (danger)
0.00%
0 / 20
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 / 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
126    if ($keywords) {
127      $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords));
128    }
129
130    return $build;
131  }
132
133  /**
134   * Build placeholder.
135   *
136   * @param string $builder_id
137   *   The builder id.
138   * @param string $label
139   *   The placeholder label.
140   * @param array $vals
141   *   HTMX vals data if the placeholder is triggering something when moving.
142   * @param \Drupal\Core\Url $preview_url
143   *   The preview_url prop value.
144   * @param string|null $keywords
145   *   (Optional) Keywords attributes to add used by search.
146   *
147   * @return array
148   *   A renderable array.
149   */
150  protected function buildPlaceholderButtonWithPreview(string $builder_id, string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL): array {
151    $build = $this->buildPlaceholderButton($label, $vals, $keywords);
152
153    $hide_script = \sprintf('Drupal.displayBuilder.hidePreview(%s)', $builder_id);
154    $attributes = [
155      'hx-get' => $preview_url->toString(),
156      'hx-target' => \sprintf('#preview-%s', $builder_id),
157      'hx-trigger' => 'mouseover delay:250ms',
158      'hx-on:mouseover' => \sprintf('Drupal.displayBuilder.showPreview(%s, this)', $builder_id),
159      'hx-on:mousedown' => $hide_script,
160      'hx-on:mouseout' => $hide_script,
161    ];
162
163    $build['#attributes'] = \array_merge($build['#attributes'], $attributes);
164
165    return $build;
166  }
167
168  /**
169   * Build placeholder.
170   *
171   * @param string $label
172   *   The placeholder label.
173   * @param array $vals
174   *   HTMX vals data if the placeholder is triggering something when moving.
175   * @param \Drupal\Core\Url $preview_url
176   *   The preview_url prop value.
177   * @param string|null $keywords
178   *   (Optional) Keywords attributes to add used by search.
179   * @param string|null $thumbnail
180   *   (Optional) The thumbnail URL.
181   *
182   * @return array
183   *   A renderable array.
184   */
185  protected function buildPlaceholderCardWithPreview(string|TranslatableMarkup $label, array $vals, Url $preview_url, ?string $keywords = NULL, ?string $thumbnail = NULL): array {
186    $build = $this->buildPlaceholder($label, '', $vals);
187    $build['#props']['preview_url'] = $preview_url;
188
189    if ($thumbnail) {
190      $build['#slots']['image'] = [
191        '#type' => 'html_tag',
192        '#tag' => 'img',
193        '#attributes' => [
194          // @todo generate proper relative url.
195          'src' => '/' . $thumbnail,
196        ],
197      ];
198    }
199
200    if ($keywords) {
201      $build['#attributes']['data-keywords'] = \trim(\strtolower($keywords));
202    }
203
204    return $build;
205  }
206
207  /**
208   * Build a button.
209   *
210   * Uniq id is required for keyboard mapping with ajax requests.
211   *
212   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
213   *   The button label.
214   * @param string $action
215   *   (Optional) The action value attribute. Used mainly for e2e tests.
216   * @param string|null $icon
217   *   (Optional) The icon name. Default none.
218   * @param string|TranslatableMarkup|null $tooltip
219   *   (Optional) Enable the tooltip feature. Default no tooltip.
220   * @param array|null $keyboard
221   *   (Optional) Keyboard shortcut as associative array key => description.
222   *
223   * @return array
224   *   The button render array.
225   */
226  protected function buildButton(
227    string|TranslatableMarkup $label,
228    ?string $action,
229    ?string $icon = NULL,
230    null|string|TranslatableMarkup $tooltip = NULL,
231    ?array $keyboard = NULL,
232  ): array {
233    $button = [
234      '#type' => 'component',
235      '#component' => 'display_builder:button',
236      '#props' => [
237        'label' => $label,
238        'icon' => $icon,
239        'tooltip' => $tooltip,
240      ],
241    ];
242
243    if ($keyboard) {
244      $button['#attributes']['data-keyboard-key'] = \key($keyboard);
245      $button['#attributes']['data-keyboard-help'] = \reset($keyboard) ?? '';
246    }
247
248    // Used to ease e2e tests.
249    if ($action) {
250      $button['#attributes']['data-island-action'] = $action;
251    }
252
253    return $button;
254  }
255
256  /**
257   * Build an icon button.
258   *
259   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
260   *   The button label.
261   * @param string|null $icon
262   *   (Optional) The icon name. Default none.
263   *
264   * @return array
265   *   The icon button render array.
266   *
267   * @todo never used, replace existing buildButton() where relevant
268   */
269  protected function buildIconButton(string|TranslatableMarkup $label, ?string $icon = NULL): array {
270    return [
271      '#type' => 'component',
272      '#component' => 'display_builder:icon_button',
273      '#props' => [
274        'icon' => $icon ?? '',
275        'label' => $label,
276      ],
277    ];
278  }
279
280  /**
281   * Build a menu item.
282   *
283   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $title
284   *   The menu title.
285   * @param string $value
286   *   The menu value.
287   * @param string|null $icon
288   *   (Optional) The icon name. Default none.
289   * @param string $icon_position
290   *   (Optional) The icon position. Default 'prefix'.
291   * @param bool $disabled
292   *   (Optional) Is the menu disabled? Default no.
293   *
294   * @return array
295   *   The menu item render array.
296   */
297  protected function buildMenuItem(
298    string|TranslatableMarkup $title,
299    string $value,
300    ?string $icon = NULL,
301    string $icon_position = 'prefix',
302    bool $disabled = FALSE,
303  ): array {
304    return [
305      '#type' => 'component',
306      '#component' => 'display_builder:menu_item',
307      '#props' => [
308        'title' => $title,
309        'value' => $value,
310        'icon' => $icon,
311        'icon_position' => $icon_position,
312        'disabled' => $disabled,
313      ],
314      '#attributes' => [
315        'data-contextual-menu' => TRUE,
316      ],
317    ];
318  }
319
320  /**
321   * Build a menu item divider.
322   *
323   * @return array
324   *   The menu item render array.
325   */
326  protected function buildMenuDivider(): array {
327    return [
328      '#type' => 'component',
329      '#component' => 'display_builder:menu_item',
330      '#props' => [
331        'variant' => 'divider',
332      ],
333    ];
334  }
335
336  /**
337   * Build draggables placeholders.
338   *
339   * Used in library islands.
340   *
341   * @param string $builder_id
342   *   Builder ID.
343   * @param array $draggables
344   *   Draggable placeholders.
345   * @param string $variant
346   *   (Optional) The variant.
347   *
348   * @return array
349   *   The draggables render array.
350   */
351  protected function buildDraggables(string $builder_id, array $draggables, string $variant = ''): array {
352    $build = [
353      '#type' => 'component',
354      '#component' => 'display_builder:draggables',
355      '#slots' => [
356        'content' => $draggables,
357      ],
358      '#attributes' => [
359        // Required for JavaScript @see components/draggables/draggables.js.
360        'data-db-id' => $builder_id,
361      ],
362    ];
363
364    if ($variant) {
365      $build['#props']['variant'] = $variant;
366    }
367
368    return $build;
369  }
370
371  /**
372   * Build tabs.
373   *
374   * @param string $id
375   *   The ID. Used for saving active tab in local storage.
376   * @param array $tabs
377   *   Tabs as links.
378   * @param bool $contextual
379   *   (Optional) Is the tabs contextual? Default no.
380   *
381   * @return array
382   *   The tabs render array.
383   */
384  protected function buildTabs(string $id, array $tabs, bool $contextual = FALSE): array {
385    $build = [
386      '#type' => 'component',
387      '#component' => 'display_builder:tabs',
388      '#props' => [
389        'tabs' => $tabs,
390        'contextual' => $contextual,
391      ],
392    ];
393
394    if ($id) {
395      $build['#props']['id'] = $id;
396    }
397
398    return $build;
399  }
400
401  /**
402   * Build input.
403   *
404   * @param string $id
405   *   The ID. Used for saving active tab in local storage.
406   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
407   *   The label.
408   * @param string $type
409   *   The input type.
410   * @param string $size
411   *   (Optional) The input size. Default medium.
412   * @param string|null $autocomplete
413   *   (Optional) The input autocomplete.
414   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $placeholder
415   *   (Optional) The input placeholder.
416   * @param bool|null $clearable
417   *   (Optional) The input clearable.
418   * @param string|null $icon
419   *   (Optional) The input icon.
420   *
421   * @return array
422   *   The input render array.
423   */
424  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 {
425    $build = [
426      '#type' => 'component',
427      '#component' => 'display_builder:input',
428      '#props' => [
429        'label' => $label,
430        'variant' => $type,
431        'size' => $size,
432      ],
433    ];
434
435    if ($id) {
436      $build['#props']['id'] = $id;
437    }
438
439    if ($autocomplete) {
440      $build['#props']['autocomplete'] = $autocomplete;
441    }
442
443    if ($placeholder) {
444      $build['#props']['placeholder'] = $placeholder;
445    }
446
447    if ($clearable) {
448      $build['#props']['clearable'] = TRUE;
449    }
450
451    if ($icon) {
452      $build['#props']['icon'] = $icon;
453    }
454
455    return $build;
456  }
457
458  /**
459   * Wraps a renderable in a div.
460   *
461   * Commonly used with tabs.
462   *
463   * @param array $content
464   *   The renderable content.
465   * @param string $id
466   *   (Optional) The div id.
467   *
468   * @return array
469   *   The wrapped render array.
470   */
471  protected function wrapContent(array $content, string $id = ''): array {
472    $build = [
473      '#type' => 'html_tag',
474      '#tag' => 'div',
475      'content' => $content,
476    ];
477
478    if (!empty($id)) {
479      $build['#attributes']['id'] = $id;
480    }
481
482    return $build;
483  }
484
485}