Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
42.59% covered (danger)
42.59%
69 / 162
37.50% covered (danger)
37.50%
21 / 56
10.53% covered (danger)
10.53%
8 / 76
16.67% covered (danger)
16.67%
2 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
RenderableBuilderTrait
42.59% covered (danger)
42.59%
69 / 162
37.50% covered (danger)
37.50%
21 / 56
10.53% covered (danger)
10.53%
8 / 76
16.67% covered (danger)
16.67%
2 / 12
862.03
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
 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 a menu item.
263   *
264   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $title
265   *   The menu title.
266   * @param string $value
267   *   The menu value.
268   * @param string|null $icon
269   *   (Optional) The icon name. Default none.
270   * @param string $icon_position
271   *   (Optional) The icon position. Default 'prefix'.
272   * @param bool $disabled
273   *   (Optional) Is the menu disabled? Default no.
274   *
275   * @return array
276   *   The menu item render array.
277   */
278  protected function buildMenuItem(
279    string|TranslatableMarkup $title,
280    string $value,
281    ?string $icon = NULL,
282    string $icon_position = 'prefix',
283    bool $disabled = FALSE,
284  ): array {
285    return [
286      '#type' => 'component',
287      '#component' => 'display_builder:menu_item',
288      '#props' => [
289        'title' => $title,
290        'value' => $value,
291        'icon' => $icon,
292        'icon_position' => $icon_position,
293        'disabled' => $disabled,
294      ],
295      '#attributes' => [
296        'data-contextual-menu' => TRUE,
297      ],
298    ];
299  }
300
301  /**
302   * Build a menu item divider.
303   *
304   * @return array
305   *   The menu item render array.
306   */
307  protected function buildMenuDivider(): array {
308    return [
309      '#type' => 'component',
310      '#component' => 'display_builder:menu_item',
311      '#props' => [
312        'variant' => 'divider',
313      ],
314    ];
315  }
316
317  /**
318   * Build draggables placeholders.
319   *
320   * Used in library islands.
321   *
322   * @param string $builder_id
323   *   Builder ID.
324   * @param array $draggables
325   *   Draggable placeholders.
326   * @param string $variant
327   *   (Optional) The variant.
328   *
329   * @return array
330   *   The draggables render array.
331   */
332  protected function buildDraggables(string $builder_id, array $draggables, string $variant = ''): array {
333    $build = [
334      '#type' => 'component',
335      '#component' => 'display_builder:draggables',
336      '#slots' => [
337        'content' => $draggables,
338      ],
339      '#attributes' => [
340        // Required for JavaScript @see components/draggables/draggables.js.
341        'data-db-id' => $builder_id,
342      ],
343    ];
344
345    if ($variant) {
346      $build['#props']['variant'] = $variant;
347    }
348
349    return $build;
350  }
351
352  /**
353   * Build tabs.
354   *
355   * @param string $id
356   *   The ID. Used for saving active tab in local storage.
357   * @param array $tabs
358   *   Tabs as links.
359   * @param bool $contextual
360   *   (Optional) Is the tabs contextual? Default no.
361   *
362   * @return array
363   *   The tabs render array.
364   */
365  protected function buildTabs(string $id, array $tabs, bool $contextual = FALSE): array {
366    $build = [
367      '#type' => 'component',
368      '#component' => 'display_builder:tabs',
369      '#props' => [
370        'tabs' => $tabs,
371        'contextual' => $contextual,
372      ],
373    ];
374
375    if ($id) {
376      $build['#props']['id'] = $id;
377    }
378
379    return $build;
380  }
381
382  /**
383   * Build input.
384   *
385   * @param string $id
386   *   The ID. Used for saving active tab in local storage.
387   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
388   *   The label.
389   * @param string $type
390   *   The input type.
391   * @param string $size
392   *   (Optional) The input size. Default medium.
393   * @param string|null $autocomplete
394   *   (Optional) The input autocomplete.
395   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $placeholder
396   *   (Optional) The input placeholder.
397   * @param bool|null $clearable
398   *   (Optional) The input clearable.
399   * @param string|null $icon
400   *   (Optional) The input icon.
401   *
402   * @return array
403   *   The input render array.
404   */
405  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 {
406    $build = [
407      '#type' => 'component',
408      '#component' => 'display_builder:input',
409      '#props' => [
410        'label' => $label,
411        'variant' => $type,
412        'size' => $size,
413      ],
414    ];
415
416    if ($id) {
417      $build['#props']['id'] = $id;
418    }
419
420    if ($autocomplete) {
421      $build['#props']['autocomplete'] = $autocomplete;
422    }
423
424    if ($placeholder) {
425      $build['#props']['placeholder'] = $placeholder;
426    }
427
428    if ($clearable) {
429      $build['#props']['clearable'] = TRUE;
430    }
431
432    if ($icon) {
433      $build['#props']['icon'] = $icon;
434    }
435
436    return $build;
437  }
438
439  /**
440   * Wraps a renderable in a div.
441   *
442   * Commonly used with tabs.
443   *
444   * @param array $content
445   *   The renderable content.
446   * @param string $id
447   *   (Optional) The div id.
448   *
449   * @return array
450   *   The wrapped render array.
451   */
452  protected function wrapContent(array $content, string $id = ''): array {
453    $build = [
454      '#type' => 'html_tag',
455      '#tag' => 'div',
456      'content' => $content,
457    ];
458
459    if (!empty($id)) {
460      $build['#attributes']['id'] = $id;
461    }
462
463    return $build;
464  }
465
466}