Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
42.57% covered (danger)
42.57%
86 / 202
47.37% covered (danger)
47.37%
45 / 95
0.26% covered (danger)
0.26%
11 / 4153
31.25% covered (danger)
31.25%
5 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
BuilderPanel
44.33% covered (danger)
44.33%
86 / 194
47.37% covered (danger)
47.37%
45 / 95
0.26% covered (danger)
0.26%
11 / 4153
31.25% covered (danger)
31.25%
5 / 16
2946.89
0.00% covered (danger)
0.00%
0 / 1
 create
100.00% covered (success)
100.00%
5 / 5
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
 keyboardShortcuts
0.00% covered (danger)
0.00%
0 / 4
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
 build
100.00% covered (success)
100.00%
17 / 17
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
 onAttachToSlot
0.00% covered (danger)
0.00%
0 / 1
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
 onUpdate
0.00% covered (danger)
0.00%
0 / 1
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
 onDelete
0.00% covered (danger)
0.00%
0 / 3
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
 buildSingleComponent
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 resolveComponentInfo
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 buildSingleBlock
92.50% covered (success)
92.50%
37 / 40
81.82% covered (warning)
81.82%
27 / 33
0.10% covered (danger)
0.10%
4 / 4096
0.00% covered (danger)
0.00%
0 / 1
305.15
 replaceNode
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 useAttributesVariable
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
 renderSource
21.05% covered (danger)
21.05%
4 / 19
44.44% covered (danger)
44.44%
4 / 9
12.50% covered (danger)
12.50%
1 / 8
0.00% covered (danger)
0.00%
0 / 1
21.75
 digFromSlot
61.90% covered (warning)
61.90%
13 / 21
60.00% covered (warning)
60.00%
9 / 15
7.69% covered (danger)
7.69%
1 / 13
0.00% covered (danger)
0.00%
0 / 1
45.54
 hasMultipleRoot
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
 isEmpty
100.00% covered (success)
100.00%
2 / 2
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
 buildComponentSlot
0.00% covered (danger)
0.00%
0 / 18
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\Plugin\display_builder\Island;
6
7use Drupal\Component\Utility\Html;
8use Drupal\Core\Render\RendererInterface;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\display_builder\Attribute\Island;
11use Drupal\display_builder\InstanceInterface;
12use Drupal\display_builder\Island\IslandPluginBase;
13use Drupal\display_builder\Island\IslandReloadEventsTrait;
14use Drupal\display_builder\Island\IslandType;
15use Drupal\display_builder\SlotSourceProxy;
16use Drupal\display_builder\SourceWithSlotsInterface;
17use Drupal\ui_patterns\Element\ComponentElementBuilder;
18use Drupal\ui_patterns\SourcePluginBase;
19use Drupal\ui_patterns\SourceWithChoicesInterface;
20use Masterminds\HTML5;
21use Symfony\Component\DependencyInjection\ContainerInterface;
22
23/**
24 * Builder island plugin implementation.
25 */
26#[Island(
27  id: 'builder',
28  enabled_by_default: TRUE,
29  label: new TranslatableMarkup('Builder'),
30  description: new TranslatableMarkup('The Display Builder main island. Build the display with dynamic preview.'),
31  type: IslandType::View,
32  default_region: 'main',
33  icon: 'tools',
34)]
35class BuilderPanel extends IslandPluginBase {
36
37  use IslandReloadEventsTrait;
38
39  /**
40   * The renderer service.
41   */
42  protected RendererInterface $renderer;
43
44  /**
45   * Proxy for slot source operations.
46   */
47  protected SlotSourceProxy $slotSourceProxy;
48
49  /**
50   * The component element builder.
51   */
52  protected ComponentElementBuilder $componentElementBuilder;
53
54  /**
55   * {@inheritdoc}
56   */
57  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
58    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
59    $instance->renderer = $container->get('renderer');
60    $instance->slotSourceProxy = $container->get('display_builder.slot_sources_proxy');
61    $instance->componentElementBuilder = $container->get('ui_patterns.component_element_builder');
62
63    return $instance;
64  }
65
66  /**
67   * {@inheritdoc}
68   */
69  public static function keyboardShortcuts(): array {
70    return [
71      'key' => 'b',
72      'help' => t('Show the builder'),
73    ];
74  }
75
76  /**
77   * {@inheritdoc}
78   */
79  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
80    $builder_id = (string) $builder->id();
81    $build = [
82      '#type' => 'component',
83      '#component' => 'display_builder:dropzone',
84      '#props' => [
85        'variant' => 'root',
86      ],
87      '#slots' => [
88        'content' => $this->digFromSlot($builder_id, $data),
89      ],
90      '#attributes' => [
91        // Required for JavaScript @see components/dropzone/dropzone.js.
92        'data-db-id' => $builder_id,
93        'data-node-title' => $this->t('Base container'),
94        'data-db-root' => TRUE,
95      ],
96    ];
97
98    return $this->htmxEvents->onRootDrop($build, $builder_id, $this->getPluginID());
99  }
100
101  /**
102   * {@inheritdoc}
103   */
104  public function onAttachToSlot(InstanceInterface $instance, string $node_id, string $parent_id): array {
105    return $this->replaceNode($instance, $parent_id);
106  }
107
108  /**
109   * {@inheritdoc}
110   */
111  public function onUpdate(InstanceInterface $instance, string $node_id): array {
112    return $this->replaceNode($instance, $node_id);
113  }
114
115  /**
116   * {@inheritdoc}
117   */
118  public function onDelete(InstanceInterface $instance, ?string $parent_id): array {
119    if (empty($parent_id)) {
120      return $this->reloadWithGlobalData($instance);
121    }
122
123    return $this->replaceNode($instance, $parent_id);
124  }
125
126  /**
127   * Build renderable from state data.
128   *
129   * @param string $builder_id
130   *   Display Builder ID.
131   * @param string $instance_id
132   *   The instance ID.
133   * @param \Drupal\display_builder\SourceWithSlotsInterface $source
134   *   The source plugin.
135   * @param array $data
136   *   The UI Patterns form state data.
137   * @param int $index
138   *   (Optional) The index of the block. Default to 0.
139   *
140   * @return array|null
141   *   A renderable array.
142   */
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
147      return NULL;
148    }
149
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
165      // Keep the placeholder if the component is not renderable.
166      $message = $component_id . ': ' . $this->t('Empty by default. Configure it to make it visible');
167      $build = $this->buildPlaceholder($message);
168    }
169
170    if (!$this->useAttributesVariable($build)) {
171      $build = $this->wrapContent($build);
172    }
173
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
176
177  /**
178   * Resolves component ID, label, and instance ID from source and data.
179   *
180   * Extracts the shared preamble logic used by all buildSingleComponent()
181   * implementations across BuilderPanel, LayersPanel, and TreePanel.
182   *
183   * @param \Drupal\display_builder\SourceWithSlotsInterface $source
184   *   The source plugin.
185   * @param array $data
186   *   The UI Patterns form state data.
187   * @param string $instance_id
188   *   The instance ID. May be empty; resolved from data['node_id'] as fallback.
189   *
190   * @return array{component_id: string, label: string, instance_id: string}|null
191   *   Associative array with 'component_id', 'label', 'instance_id', or NULL
192   *   if either component_id or instance_id could not be resolved.
193   */
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
199      $component_id = $source->getChoice($data['source']);
200      $result = $this->slotSourceProxy->getLabelWithSummary($data, [], TRUE);
201      $label = $result['label'] ?? $source->label();
202    }
203
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
207      $this->logger->error(
208        '[' . static::class . '::buildSingleComponent] missing component ID: @component_id or instance ID: @instance_id. <pre>' . \print_r($data, TRUE) . '</pre>',
209        ['@instance_id' => $instance_id ?? 'NULL', '@component_id' => $component_id],
210      );
211
212      return NULL;
213    }
214
215    return [
216      'component_id' => $component_id,
217      'label' => $label,
218      'instance_id' => $instance_id,
219    ];
220  }
221
222  /**
223   * Build renderable from state data.
224   *
225   * @param string $builder_id
226   *   Display Builder ID.
227   * @param string $instance_id
228   *   The instance ID.
229   * @param array $data
230   *   The UI Patterns form state data.
231   * @param int $index
232   *   (Optional) The index of the block. Default to 0.
233   *
234   * @return array|null
235   *   A renderable array.
236   */
237  protected function buildSingleBlock(string $builder_id, string $instance_id, array $data, int $index = 0): ?array {
238    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
239
240    if (!$instance_id) {
241      return NULL;
242    }
243
244    $classes = ['db-block'];
245
246    if (isset($data['source']['plugin_id'])) {
247      $classes[] = 'db-block-' . \strtolower(Html::cleanCssIdentifier($data['source']['plugin_id']));
248    }
249    else {
250      $classes[] = 'db-block-' . \strtolower(Html::cleanCssIdentifier($data['source_id']));
251    }
252    $build = $this->renderSource($data, $classes);
253    $is_empty = FALSE;
254
255    if (isset($data['source_id']) && $data['source_id'] === 'token') {
256      if (isset($build['content']) && empty($build['content'])) {
257        $is_empty = TRUE;
258      }
259    }
260
261    if (($data['source']['plugin_id'] ?? '') === 'system_messages_block') {
262      // system_messages_block is never empty, but often invisible.
263      // See: core/modules/system/src/Plugin/Block/SystemMessagesBlock.php
264      // See: core/lib/Drupal/Core/Render/Element/StatusMessages.php
265      // Let's always display it in a placeholder.
266      $is_empty = TRUE;
267    }
268
269    $label_info = $this->slotSourceProxy->getLabelWithSummary($data, $this->configuration['contexts'] ?? []);
270
271    if (isset($data['source_id'])) {
272      switch ($data['source_id']) {
273        case 'entity_field':
274          $label_info['summary'] = (string) $this->t('Field: @label', ['@label' => $label_info['label']]);
275
276          break;
277
278        case 'block':
279          $label_info['summary'] = (string) $this->t('Block: @label', ['@label' => $label_info['summary']]);
280
281          break;
282      }
283    }
284
285    // This is the placeholder without configuration or content yet.
286    if ($this->isEmpty($build) || $is_empty) {
287      $build = $this->buildPlaceholderButton($label_info['summary']);
288      // Highlight in the view to show it's a temporary block waiting for
289      // configuration.
290      $build['#attributes']['class'][] = 'db-background';
291    }
292    elseif (!$this->useAttributesVariable($build) || $this->hasMultipleRoot($build)) {
293      $build = [
294        '#type' => 'html_tag',
295        '#tag' => 'div',
296        '#attributes' => ['class' => $classes],
297        'content' => $build,
298      ];
299    }
300
301    // This label is used for contextual menu.
302    // @see assets/js/contextual_menu.js
303    // The 'data-node-title' attribute is expected to contain a human-readable
304    // label or summary describing the block instance. This value is usd in the
305    // contextual menu for user actions such as edit, delete. The format should
306    // be a plain string, typically the label or field summary.
307    $build['#attributes']['data-node-title'] = $label_info['summary'] ?? $data['source_id'] ?? $data['node_id'] ?? '';
308    $build['#attributes']['data-slot-position'] = $index;
309    $build['#attributes']['data-instance-id'] = $instance_id;
310
311    // Add data-node-type for easier identification of block types in JS or CSS.
312    if (isset($data['source_id'])) {
313      $build['#attributes']['data-node-type'] = $data['source_id'];
314    }
315
316    $build = $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $label_info['summary'] ?? $label_info['label'] ?? '', $index);
317
318    return $build;
319  }
320
321  /**
322   * Helper method to replace a specific instance in the DOM.
323   *
324   * @param \Drupal\display_builder\InstanceInterface $instance
325   *   The builder instance.
326   * @param string $node_id
327   *   The node ID from the source tree.
328   *
329   * @return array
330   *   Returns a render array with out-of-band commands.
331   */
332  protected function replaceNode(InstanceInterface $instance, string $node_id): array {
333    $builder_id = (string) $instance->id();
334    $parent_selector = '#' . $this->getHtmlId($builder_id) . ' [data-node-id="' . $node_id . '"]';
335    $data = $instance->getNode($node_id);
336    $build = [];
337    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
338
339    try {
340      $source = $this->sourceManager->createInstance(
341        $data['source_id'],
342        SourcePluginBase::buildConfiguration('slot', $slot_definition, $data, $this->configuration['contexts'] ?? [])
343      );
344    }
345    catch (\Throwable $e) {
346      $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
347
348      return [];
349    }
350
351    if ($source instanceof SourceWithSlotsInterface) {
352      $build = $this->buildSingleComponent($builder_id, $node_id, $source, $data);
353    }
354    else {
355      $build = $this->buildSingleBlock($builder_id, $node_id, $data);
356    }
357
358    return $this->makeOutOfBand(
359      $build ?? [],
360      $parent_selector,
361      'outerHTML'
362    );
363  }
364
365  /**
366   * Does the component use the attributes variable in template?
367   *
368   * @param array $renderable
369   *   Component renderable.
370   *
371   * @return bool
372   *   Use it or not.
373   */
374  protected function useAttributesVariable(array $renderable): bool {
375    $random = \uniqid();
376    $renderable['#attributes'][$random] = $random;
377    $html = $this->renderer->renderInIsolation($renderable);
378
379    return \str_contains((string) $html, $random);
380  }
381
382  /**
383   * Get renderable array for a slot source.
384   *
385   * @param array $data
386   *   The slot source data array containing:
387   *   - source_id: The source ID
388   *   - source: Array of source configuration.
389   * @param array $classes
390   *   (Optional) Classes to use to wrap the rendered source if needed.
391   *
392   * @return array
393   *   The renderable array for this slot source.
394   */
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
401      return $build;
402    }
403
404    // If token is only markup, we don't have a wrapper, add it like styles
405    // so the placeholder can be styled.
406    if (!isset($build['#type'])) {
407      $build = [
408        '#type' => 'html_tag',
409        '#tag' => 'div',
410        '#attributes' => ['class' => $classes],
411        'content' => $build,
412      ];
413    }
414
415    // If a style is applied, we have a wrapper from styles with classes, to
416    // avoid our placeholder classes to be replaced we need to wrap it.
417    elseif (isset($build['#attributes'])) {
418      $build = [
419        '#type' => 'html_tag',
420        '#tag' => 'div',
421        '#attributes' => ['class' => $classes],
422        'content' => $build,
423      ];
424    }
425
426    return $build;
427  }
428
429  /**
430   * Build builder renderable, recursively.
431   *
432   * @param string $builder_id
433   *   Builder ID.
434   * @param array $data
435   *   The current 'slice' of data.
436   *
437   * @return array
438   *   A renderable array.
439   */
440  protected function digFromSlot(string $builder_id, array $data): array {
441    $renderable = [];
442    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
443
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
480
481  /**
482   * Check if a renderable has multiple HTML root elements once rendered.
483   *
484   * @param array $renderable
485   *   The renderable array to check.
486   *
487   * @return bool
488   *   TRUE if the rendered output has multiple root elements, FALSE otherwise.
489   */
490  private function hasMultipleRoot(array $renderable): bool {
491    $html = (string) $this->renderer->renderInIsolation($renderable);
492    $dom = new HTML5(['disable_html_ns' => TRUE, 'encoding' => 'UTF-8']);
493    $dom = $dom->loadHTMLFragment($html);
494
495    return $dom->childElementCount > 1;
496  }
497
498  /**
499   * Check if a renderable array is empty.
500   *
501   * @param array $renderable
502   *   The renderable array to check.
503   *
504   * @return bool
505   *   TRUE if the rendered output is empty, FALSE otherwise.
506   */
507  private function isEmpty(array $renderable): bool {
508    $html = $this->renderer->renderInIsolation($renderable);
509
510    return empty(\trim((string) $html));
511  }
512
513  /**
514   * Build a component slot with dropzone.
515   *
516   * @param string $builder_id
517   *   The builder ID.
518   * @param \Drupal\display_builder\SourceWithSlotsInterface $source
519   *   The source plugin.
520   * @param string $slot_id
521   *   The slot ID.
522   * @param array $definition
523   *   The slot definition.
524   * @param string $instance_id
525   *   The instance ID.
526   *
527   * @return array
528   *   A renderable array for the slot.
529   */
530  private function buildComponentSlot(string $builder_id, SourceWithSlotsInterface $source, string $slot_id, array $definition, string $instance_id): array {
531    $dropzone = [
532      '#type' => 'component',
533      '#component' => 'display_builder:dropzone',
534      '#props' => [
535        'title' => $definition['title'],
536        'variant' => 'highlighted',
537      ],
538      '#attributes' => [
539        // Required for JavaScript @see components/dropzone/dropzone.js.
540        'data-db-id' => $builder_id,
541        // Slot is needed for contextual menu paste.
542        // @see assets/js/contextual_menu.js
543        'data-slot-id' => $slot_id,
544        'data-slot-title' => \ucfirst($definition['title']),
545        'data-node-id' => $instance_id,
546        'data-instance-id' => $instance_id . '_' . $slot_id,
547      ],
548    ];
549
550    if ($sources = $source->getSlotValue($slot_id)) {
551      $dropzone['#slots']['content'] = $this->digFromSlot($builder_id, $sources);
552    }
553
554    return $this->htmxEvents->onSlotDrop($dropzone, $builder_id, $this->getPluginID(), $instance_id, $slot_id);
555  }
556
557}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

BuilderPanel->build
79  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
80    $builder_id = (string) $builder->id();
81    $build = [
82      '#type' => 'component',
83      '#component' => 'display_builder:dropzone',
84      '#props' => [
85        'variant' => 'root',
86      ],
87      '#slots' => [
88        'content' => $this->digFromSlot($builder_id, $data),
89      ],
90      '#attributes' => [
91        // Required for JavaScript @see components/dropzone/dropzone.js.
92        'data-db-id' => $builder_id,
93        'data-node-title' => $this->t('Base container'),
94        'data-db-root' => TRUE,
95      ],
96    ];
97
98    return $this->htmxEvents->onRootDrop($build, $builder_id, $this->getPluginID());
99  }
BuilderPanel->buildComponentSlot
530  private function buildComponentSlot(string $builder_id, SourceWithSlotsInterface $source, string $slot_id, array $definition, string $instance_id): array {
531    $dropzone = [
532      '#type' => 'component',
533      '#component' => 'display_builder:dropzone',
534      '#props' => [
535        'title' => $definition['title'],
536        'variant' => 'highlighted',
537      ],
538      '#attributes' => [
539        // Required for JavaScript @see components/dropzone/dropzone.js.
540        'data-db-id' => $builder_id,
541        // Slot is needed for contextual menu paste.
542        // @see assets/js/contextual_menu.js
543        'data-slot-id' => $slot_id,
544        'data-slot-title' => \ucfirst($definition['title']),
545        'data-node-id' => $instance_id,
546        'data-instance-id' => $instance_id . '_' . $slot_id,
547      ],
548    ];
549
550    if ($sources = $source->getSlotValue($slot_id)) {
 
551      $dropzone['#slots']['content'] = $this->digFromSlot($builder_id, $sources);
552    }
553
554    return $this->htmxEvents->onSlotDrop($dropzone, $builder_id, $this->getPluginID(), $instance_id, $slot_id);
 
554    return $this->htmxEvents->onSlotDrop($dropzone, $builder_id, $this->getPluginID(), $instance_id, $slot_id);
555  }
530  private function buildComponentSlot(string $builder_id, SourceWithSlotsInterface $source, string $slot_id, array $definition, string $instance_id): array {
531    $dropzone = [
532      '#type' => 'component',
533      '#component' => 'display_builder:dropzone',
534      '#props' => [
535        'title' => $definition['title'],
536        'variant' => 'highlighted',
537      ],
538      '#attributes' => [
539        // Required for JavaScript @see components/dropzone/dropzone.js.
540        'data-db-id' => $builder_id,
541        // Slot is needed for contextual menu paste.
542        // @see assets/js/contextual_menu.js
543        'data-slot-id' => $slot_id,
544        'data-slot-title' => \ucfirst($definition['title']),
545        'data-node-id' => $instance_id,
546        'data-instance-id' => $instance_id . '_' . $slot_id,
547      ],
548    ];
549
550    if ($sources = $source->getSlotValue($slot_id)) {
 
554    return $this->htmxEvents->onSlotDrop($dropzone, $builder_id, $this->getPluginID(), $instance_id, $slot_id);
555  }
BuilderPanel->buildSingleComponent
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
147      return NULL;
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
166      $message = $component_id . ': ' . $this->t('Empty by default. Configure it to make it visible');
167      $build = $this->buildPlaceholder($message);
168    }
169
170    if (!$this->useAttributesVariable($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
171      $build = $this->wrapContent($build);
172    }
173
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
166      $message = $component_id . ': ' . $this->t('Empty by default. Configure it to make it visible');
167      $build = $this->buildPlaceholder($message);
168    }
169
170    if (!$this->useAttributesVariable($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
171      $build = $this->wrapContent($build);
172    }
173
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
166      $message = $component_id . ': ' . $this->t('Empty by default. Configure it to make it visible');
167      $build = $this->buildPlaceholder($message);
168    }
169
170    if (!$this->useAttributesVariable($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
171      $build = $this->wrapContent($build);
172    }
173
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
166      $message = $component_id . ': ' . $this->t('Empty by default. Configure it to make it visible');
167      $build = $this->buildPlaceholder($message);
168    }
169
170    if (!$this->useAttributesVariable($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
171      $build = $this->wrapContent($build);
172    }
173
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
166      $message = $component_id . ': ' . $this->t('Empty by default. Configure it to make it visible');
167      $build = $this->buildPlaceholder($message);
168    }
169
170    if (!$this->useAttributesVariable($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
171      $build = $this->wrapContent($build);
172    }
173
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
166      $message = $component_id . ': ' . $this->t('Empty by default. Configure it to make it visible');
167      $build = $this->buildPlaceholder($message);
168    }
169
170    if (!$this->useAttributesVariable($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
171      $build = $this->wrapContent($build);
172    }
173
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
143  protected function buildSingleComponent(string $builder_id, string $instance_id, SourceWithSlotsInterface $source, array $data, int $index = 0): ?array {
144    $info = $this->resolveComponentInfo($source, $data, $instance_id);
145
146    if ($info === NULL) {
 
150    ['component_id' => $component_id, 'label' => $label, 'instance_id' => $instance_id] = $info;
151
152    $build = $this->renderSource($data);
153    // Required for the context menu label.
154    // @see assets/js/contextual_menu.js
155    $build['#attributes']['data-node-title'] = $label;
156    $build['#attributes']['data-slot-position'] = $index;
157    $build['#attributes']['data-instance-id'] = $instance_id;
158
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
 
159    foreach ($source->getSlotDefinitions() as $slot_id => $definition) {
160      $slot = $this->buildComponentSlot($builder_id, $source, $slot_id, $definition, $instance_id);
161      $build = $source->setSlotRenderable($build, $slot_id, $slot);
162    }
163
164    if ($this->isEmpty($build)) {
 
170    if (!$this->useAttributesVariable($build)) {
 
174    return $this->htmxEvents->onInstanceClick($build, $builder_id, $instance_id, $source->label(), $index);
175  }
BuilderPanel->create
57  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
58    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
59    $instance->renderer = $container->get('renderer');
60    $instance->slotSourceProxy = $container->get('display_builder.slot_sources_proxy');
61    $instance->componentElementBuilder = $container->get('ui_patterns.component_element_builder');
62
63    return $instance;
64  }
BuilderPanel->digFromSlot
440  protected function digFromSlot(string $builder_id, array $data): array {
441    $renderable = [];
442    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
443
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
446        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
440  protected function digFromSlot(string $builder_id, array $data): array {
441    $renderable = [];
442    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
443
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
 
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
 
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
 
465          $renderable[$index] = $component;
466        }
467
468        continue;
 
468        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
440  protected function digFromSlot(string $builder_id, array $data): array {
441    $renderable = [];
442    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
443
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
 
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
 
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
 
468        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
440  protected function digFromSlot(string $builder_id, array $data): array {
441    $renderable = [];
442    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
443
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
 
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
 
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
440  protected function digFromSlot(string $builder_id, array $data): array {
441    $renderable = [];
442    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
443
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
 
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
 
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
440  protected function digFromSlot(string $builder_id, array $data): array {
441    $renderable = [];
442    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
443
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
440  protected function digFromSlot(string $builder_id, array $data): array {
441    $renderable = [];
442    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
443
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
455      catch (\Throwable $e) {
 
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
446        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
455      catch (\Throwable $e) {
 
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
 
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
 
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
 
465          $renderable[$index] = $component;
466        }
467
468        continue;
 
468        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
455      catch (\Throwable $e) {
 
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
 
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
 
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
 
468        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
455      catch (\Throwable $e) {
 
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
 
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
 
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
455      catch (\Throwable $e) {
 
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
 
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
 
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
 
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
455      catch (\Throwable $e) {
 
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
 
444    foreach ($data as $index => $source) {
 
444    foreach ($data as $index => $source) {
445      if (!isset($source['source_id'])) {
446        continue;
447      }
448
449      try {
450        $source_plugin = $this->sourceManager->createInstance(
451          $source['source_id'],
452          SourcePluginBase::buildConfiguration('slot', $slot_definition, $source, $this->configuration['contexts'] ?? [])
453        );
454      }
455      catch (\Throwable $e) {
456        $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
457
458        continue;
459      }
460
461      if ($source_plugin instanceof SourceWithSlotsInterface) {
462        $component = $this->buildSingleComponent($builder_id, '', $source_plugin, $source, $index);
463
464        if ($component) {
465          $renderable[$index] = $component;
466        }
467
468        continue;
469      }
470
471      $block = $this->buildSingleBlock($builder_id, '', $source, $index);
472
473      if ($block) {
474        $renderable[$index] = $block;
475      }
476    }
477
478    return $renderable;
479  }
BuilderPanel->hasMultipleRoot
490  private function hasMultipleRoot(array $renderable): bool {
491    $html = (string) $this->renderer->renderInIsolation($renderable);
492    $dom = new HTML5(['disable_html_ns' => TRUE, 'encoding' => 'UTF-8']);
493    $dom = $dom->loadHTMLFragment($html);
494
495    return $dom->childElementCount > 1;
496  }
BuilderPanel->isEmpty
507  private function isEmpty(array $renderable): bool {
508    $html = $this->renderer->renderInIsolation($renderable);
509
510    return empty(\trim((string) $html));
511  }
BuilderPanel->keyboardShortcuts
71      'key' => 'b',
72      'help' => t('Show the builder'),
73    ];
74  }
BuilderPanel->onAttachToSlot
104  public function onAttachToSlot(InstanceInterface $instance, string $node_id, string $parent_id): array {
105    return $this->replaceNode($instance, $parent_id);
106  }
BuilderPanel->onDelete
118  public function onDelete(InstanceInterface $instance, ?string $parent_id): array {
119    if (empty($parent_id)) {
 
120      return $this->reloadWithGlobalData($instance);
118  public function onDelete(InstanceInterface $instance, ?string $parent_id): array {
119    if (empty($parent_id)) {
 
123    return $this->replaceNode($instance, $parent_id);
124  }
BuilderPanel->onUpdate
111  public function onUpdate(InstanceInterface $instance, string $node_id): array {
112    return $this->replaceNode($instance, $node_id);
113  }
BuilderPanel->renderSource
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
401      return $build;
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
406    if (!isset($build['#type'])) {
 
406    if (!isset($build['#type'])) {
407      $build = [
408        '#type' => 'html_tag',
 
426    return $build;
427  }
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
406    if (!isset($build['#type'])) {
 
417    elseif (isset($build['#attributes'])) {
 
419        '#type' => 'html_tag',
420        '#tag' => 'div',
421        '#attributes' => ['class' => $classes],
422        'content' => $build,
423      ];
424    }
425
426    return $build;
 
426    return $build;
427  }
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
406    if (!isset($build['#type'])) {
 
417    elseif (isset($build['#attributes'])) {
 
426    return $build;
427  }
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
401      return $build;
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
406    if (!isset($build['#type'])) {
 
406    if (!isset($build['#type'])) {
407      $build = [
408        '#type' => 'html_tag',
 
426    return $build;
427  }
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
406    if (!isset($build['#type'])) {
 
417    elseif (isset($build['#attributes'])) {
 
419        '#type' => 'html_tag',
420        '#tag' => 'div',
421        '#attributes' => ['class' => $classes],
422        'content' => $build,
423      ];
424    }
425
426    return $build;
 
426    return $build;
427  }
395  protected function renderSource(array $data, array $classes = []): array {
396    $build = $this->componentElementBuilder->buildSource([], 'content', [], $data, $this->configuration['contexts'] ?? []) ?? [];
397    $build = $build['#slots']['content'][0] ?? [];
398
399    // Fixes for token which is simple markup or html.
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
400    if (isset($data['source_id']) && $data['source_id'] !== 'token') {
 
406    if (!isset($build['#type'])) {
 
417    elseif (isset($build['#attributes'])) {
 
426    return $build;
427  }
BuilderPanel->replaceNode
332  protected function replaceNode(InstanceInterface $instance, string $node_id): array {
333    $builder_id = (string) $instance->id();
334    $parent_selector = '#' . $this->getHtmlId($builder_id) . ' [data-node-id="' . $node_id . '"]';
335    $data = $instance->getNode($node_id);
336    $build = [];
337    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
338
339    try {
340      $source = $this->sourceManager->createInstance(
 
351    if ($source instanceof SourceWithSlotsInterface) {
 
351    if ($source instanceof SourceWithSlotsInterface) {
352      $build = $this->buildSingleComponent($builder_id, $node_id, $source, $data);
 
358    return $this->makeOutOfBand(
359      $build ?? [],
360      $parent_selector,
361      'outerHTML'
362    );
363  }
332  protected function replaceNode(InstanceInterface $instance, string $node_id): array {
333    $builder_id = (string) $instance->id();
334    $parent_selector = '#' . $this->getHtmlId($builder_id) . ' [data-node-id="' . $node_id . '"]';
335    $data = $instance->getNode($node_id);
336    $build = [];
337    $slot_definition = ['ui_patterns' => ['type_definition' => $this->sourceManager->getSlotPropType()]];
338
339    try {
340      $source = $this->sourceManager->createInstance(
 
351    if ($source instanceof SourceWithSlotsInterface) {
 
355      $build = $this->buildSingleBlock($builder_id, $node_id, $data);
356    }
357
358    return $this->makeOutOfBand(
 
358    return $this->makeOutOfBand(
359      $build ?? [],
360      $parent_selector,
361      'outerHTML'
362    );
363  }
345    catch (\Throwable $e) {
 
346      $this->logger->error('Invalid source found: %message', ['%message' => $e->getMessage()]);
347
348      return [];
BuilderPanel->resolveComponentInfo
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
 
199      $component_id = $source->getChoice($data['source']);
200      $result = $this->slotSourceProxy->getLabelWithSummary($data, [], TRUE);
201      $label = $result['label'] ?? $source->label();
202    }
203
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
 
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
207      $this->logger->error(
208        '[' . static::class . '::buildSingleComponent] missing component ID: @component_id or instance ID: @instance_id. <pre>' . \print_r($data, TRUE) . '</pre>',
209        ['@instance_id' => $instance_id ?? 'NULL', '@component_id' => $component_id],
210      );
211
212      return NULL;
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
 
199      $component_id = $source->getChoice($data['source']);
200      $result = $this->slotSourceProxy->getLabelWithSummary($data, [], TRUE);
201      $label = $result['label'] ?? $source->label();
202    }
203
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
 
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
216      'component_id' => $component_id,
217      'label' => $label,
218      'instance_id' => $instance_id,
219    ];
220  }
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
 
199      $component_id = $source->getChoice($data['source']);
200      $result = $this->slotSourceProxy->getLabelWithSummary($data, [], TRUE);
201      $label = $result['label'] ?? $source->label();
202    }
203
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
 
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
207      $this->logger->error(
208        '[' . static::class . '::buildSingleComponent] missing component ID: @component_id or instance ID: @instance_id. <pre>' . \print_r($data, TRUE) . '</pre>',
209        ['@instance_id' => $instance_id ?? 'NULL', '@component_id' => $component_id],
210      );
211
212      return NULL;
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
 
199      $component_id = $source->getChoice($data['source']);
200      $result = $this->slotSourceProxy->getLabelWithSummary($data, [], TRUE);
201      $label = $result['label'] ?? $source->label();
202    }
203
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
 
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
216      'component_id' => $component_id,
217      'label' => $label,
218      'instance_id' => $instance_id,
219    ];
220  }
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
 
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
207      $this->logger->error(
208        '[' . static::class . '::buildSingleComponent] missing component ID: @component_id or instance ID: @instance_id. <pre>' . \print_r($data, TRUE) . '</pre>',
209        ['@instance_id' => $instance_id ?? 'NULL', '@component_id' => $component_id],
210      );
211
212      return NULL;
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
 
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
216      'component_id' => $component_id,
217      'label' => $label,
218      'instance_id' => $instance_id,
219    ];
220  }
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
 
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
207      $this->logger->error(
208        '[' . static::class . '::buildSingleComponent] missing component ID: @component_id or instance ID: @instance_id. <pre>' . \print_r($data, TRUE) . '</pre>',
209        ['@instance_id' => $instance_id ?? 'NULL', '@component_id' => $component_id],
210      );
211
212      return NULL;
194  protected function resolveComponentInfo(SourceWithSlotsInterface $source, array $data, string $instance_id): ?array {
195    $component_id = $source->getPluginID();
196    $label = $source->label();
197
198    if ($source instanceof SourceWithChoicesInterface) {
 
204    $instance_id = $instance_id ?: $data['node_id'] ?? NULL;
205
206    if (!$instance_id || !$component_id) {
 
206    if (!$instance_id || !$component_id) {
 
216      'component_id' => $component_id,
217      'label' => $label,
218      'instance_id' => $instance_id,
219    ];
220  }
BuilderPanel->useAttributesVariable
374  protected function useAttributesVariable(array $renderable): bool {
375    $random = \uniqid();
376    $renderable['#attributes'][$random] = $random;
377    $html = $this->renderer->renderInIsolation($renderable);
378
379    return \str_contains((string) $html, $random);
380  }