Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContextualFormPanel
0.00% covered (danger)
0.00%
0 / 83
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 14
1190
0.00% covered (danger)
0.00%
0 / 1
 create
0.00% covered (danger)
0.00%
0 / 3
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
 label
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
 buildForm
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 build
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 onAttachToRoot
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
 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
 onActive
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 / 3
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 / 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
 isApplicable
0.00% covered (danger)
0.00%
0 / 1
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
 alterFormValues
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
72
 isEmpty
0.00% covered (danger)
0.00%
0 / 16
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
 isMultipleItemsSlotSource
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 removeItemSelector
0.00% covered (danger)
0.00%
0 / 3
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
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\Form\FormStateInterface;
8use Drupal\Core\Render\Element;
9use Drupal\Core\StringTranslation\TranslatableMarkup;
10use Drupal\display_builder\Attribute\Island;
11use Drupal\display_builder\InstanceInterface;
12use Drupal\display_builder\IslandPluginBase;
13use Drupal\display_builder\IslandType;
14use Drupal\display_builder\IslandWithFormInterface;
15use Drupal\display_builder\IslandWithFormTrait;
16use Drupal\display_builder\SourceWithSlotsInterface;
17use Drupal\ui_patterns\SourcePluginManager;
18use Symfony\Component\DependencyInjection\ContainerInterface;
19
20/**
21 * Instance form island plugin implementation.
22 */
23#[Island(
24  id: 'contextual_form',
25  enabled_by_default: TRUE,
26  label: new TranslatableMarkup('Contextual form'),
27  description: new TranslatableMarkup('Configure the active component or block.'),
28  type: IslandType::Contextual,
29)]
30class ContextualFormPanel extends IslandPluginBase implements IslandWithFormInterface {
31
32  use IslandWithFormTrait;
33
34  /**
35   * The UI Patterns source plugin manager.
36   */
37  protected SourcePluginManager $sourceManager;
38
39  /**
40   * {@inheritdoc}
41   */
42  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
43    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
44    $instance->sourceManager = $container->get('plugin.manager.ui_patterns_source');
45
46    return $instance;
47  }
48
49  /**
50   * {@inheritdoc}
51   */
52  public function label(): string {
53    return 'Config';
54  }
55
56  /**
57   * {@inheritdoc}
58   */
59  public function buildForm(array &$form, FormStateInterface $form_state): void {
60    try {
61      $contexts = $form_state->getBuildInfo()['args'][1] ?? [];
62
63      $this->alterFormValues($form_state);
64      $source = $this->sourceManager->getSource($this->data['node_id'], [], $this->data, $contexts);
65
66      if ($source instanceof SourceWithSlotsInterface) {
67        $form = $source->settingsFormPropsOnly([], $form_state);
68      }
69      else {
70        $form = $source ? $source->settingsForm([], $form_state) : [];
71      }
72
73      if ($this->isMultipleItemsSlotSource($this->data['source'])) {
74        $form = $this->removeItemSelector($form);
75      }
76    }
77    catch (\Exception) {
78    }
79  }
80
81  /**
82   * {@inheritdoc}
83   */
84  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
85    $build = parent::build($builder, $data, $options);
86
87    if (empty($build)) {
88      return $build;
89    }
90
91    if (self::isEmpty($build)) {
92      return [
93        '#type' => 'html_tag',
94        '#tag' => 'p',
95        '#value' => $this->t('No configuration required.'),
96        '#attributes' => [
97          'class' => ['description'],
98        ],
99      ];
100    }
101
102    $build = [
103      'source' => $build,
104    ];
105
106    $build['update'] = [
107      '#type' => 'button',
108      '#value' => 'Update',
109      '#submit_button' => FALSE,
110      '#attributes' => [
111        'type' => 'button',
112        'data-wysiwyg-fix' => TRUE,
113      ],
114    ];
115
116    $build = $this->htmxEvents->onInstanceFormChange($build, $this->builderId, $this->getPluginId(), $this->data['node_id']);
117
118    return $this->htmxEvents->onInstanceUpdateButtonClick($build, $this->builderId, $this->getPluginId(), $this->data['node_id']);
119  }
120
121  /**
122   * {@inheritdoc}
123   */
124  public function onAttachToRoot(string $builder_id, string $instance_id): array {
125    return $this->reloadWithInstanceData($builder_id, $instance_id);
126  }
127
128  /**
129   * {@inheritdoc}
130   */
131  public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array {
132    return $this->reloadWithInstanceData($builder_id, $instance_id);
133  }
134
135  /**
136   * {@inheritdoc}
137   */
138  public function onActive(string $builder_id, array $data): array {
139    return $this->reloadWithLocalData($builder_id, $data);
140  }
141
142  /**
143   * {@inheritdoc}
144   */
145  public function onUpdate(string $builder_id, string $instance_id): array {
146    // Reload the form itself on update.
147    // @todo pass \Drupal\display_builder\InstanceInterface object in
148    // parameters instead of loading again.
149    /** @var \Drupal\display_builder\InstanceInterface $builder */
150    $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
151    $data = $builder->getNode($instance_id);
152
153    return $this->reloadWithLocalData($builder_id, $data);
154  }
155
156  /**
157   * {@inheritdoc}
158   */
159  public function onDelete(string $builder_id, string $parent_id): array {
160    return $this->reloadWithLocalData($builder_id, []);
161  }
162
163  /**
164   * {@inheritdoc}
165   */
166  public function isApplicable(): bool {
167    return isset($this->data['source_id']) && isset($this->data['node_id']);
168  }
169
170  /**
171   * Alter the form values.
172   *
173   * @param \Drupal\Core\Form\FormStateInterface $form_state
174   *   The form state.
175   */
176  protected function alterFormValues(FormStateInterface $form_state): void {
177    // When this is an Ajax CALL, we directly inject the data into the source
178    // settings, but not during the rebuilt.
179    $values = $form_state->getValues();
180
181    if (isset($values['_drupal_ajax']) && $values['_drupal_ajax'] && !$form_state->isRebuilding()) {
182      if ($this->data['source_id'] !== 'component') {
183        $this->data['source'] = $values;
184      }
185    }
186
187    // When rebuilding the form, we need to inject the values into the source
188    // settings.
189    if ($form_state->isRebuilding()) {
190      // Allow to get the posted values through ajax, and give them to the
191      // source plugin through its settings (essential).
192      if (isset($values['source'])) {
193        unset($values['source']);
194      }
195
196      if (!empty($values)) {
197        $this->data['source'] = $values;
198      }
199    }
200  }
201
202  /**
203   * Indicates whether the given form array is empty.
204   *
205   * @param array $form
206   *   The form.
207   *
208   * @return bool
209   *   Whether the given element is empty.
210   */
211  private static function isEmpty(array $form) {
212    $keys = Element::children($form);
213
214    // Quick valid if a component. An empty component is a rare occurrence.
215    if (isset($keys['component'])) {
216      return FALSE;
217    }
218
219    return \array_diff(Element::children($form), [
220      'plugin_id',
221      'form_build_id',
222      'form_token',
223      'form_id',
224      // Exclude some core block with no configuration.
225      // @todo remove when we do not need the update button anymore.
226      'help_block',
227      'local_actions_block',
228      'node_syndicate_block',
229      'system_breadcrumb_block',
230      'system_clear_cache_block',
231      'system_messages_block',
232      'system_powered_by_block',
233    ]) === [];
234  }
235
236  /**
237   * Has the slot source multiple items?
238   *
239   * Some slot sources have 'multiple' items, with a select form element first,
240   * then an item specific form changing with Ajax. They have both a plugin_id
241   * key and a dynamic key with the value of the plugin_id.
242   *
243   * @param array $data
244   *   The slot source data containing:
245   *   - plugin_id: The plugin ID.
246   *
247   * @return bool
248   *   Is multiple or not.
249   */
250  private function isMultipleItemsSlotSource(array $data): bool {
251    if (!isset($data['plugin_id']) || !\is_string($data['plugin_id'])) {
252      return FALSE;
253    }
254
255    if (\count($data) === 1) {
256      // If there is only plugin_id, without any settings, it is OK.
257      return TRUE;
258    }
259    // If there are settings, we need at least the one specific to the item.
260    $plugin_id = (string) $data['plugin_id'];
261
262    if (isset($data[$plugin_id]) && \is_array($data[$plugin_id])) {
263      return TRUE;
264    }
265
266    return FALSE;
267  }
268
269  /**
270   * Remove the item selector from a form.
271   *
272   * For multiple items slot sources, we don't want to show the item selector
273   * since it is already selected in the slot configuration.
274   *
275   * @param array $form
276   *   The form array.
277   *
278   * @return array
279   *   The modified form array.
280   */
281  private function removeItemSelector(array $form): array {
282    $form['plugin_id']['#type'] = 'hidden';
283    unset($form['plugin_id']['#options']);
284
285    return $form;
286  }
287
288}

Branches

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

ContextualFormPanel->alterFormValues
176  protected function alterFormValues(FormStateInterface $form_state): void {
177    // When this is an Ajax CALL, we directly inject the data into the source
178    // settings, but not during the rebuilt.
179    $values = $form_state->getValues();
180
181    if (isset($values['_drupal_ajax']) && $values['_drupal_ajax'] && !$form_state->isRebuilding()) {
181    if (isset($values['_drupal_ajax']) && $values['_drupal_ajax'] && !$form_state->isRebuilding()) {
181    if (isset($values['_drupal_ajax']) && $values['_drupal_ajax'] && !$form_state->isRebuilding()) {
181    if (isset($values['_drupal_ajax']) && $values['_drupal_ajax'] && !$form_state->isRebuilding()) {
181    if (isset($values['_drupal_ajax']) && $values['_drupal_ajax'] && !$form_state->isRebuilding()) {
182      if ($this->data['source_id'] !== 'component') {
183        $this->data['source'] = $values;
184      }
185    }
186
187    // When rebuilding the form, we need to inject the values into the source
188    // settings.
189    if ($form_state->isRebuilding()) {
189    if ($form_state->isRebuilding()) {
192      if (isset($values['source'])) {
193        unset($values['source']);
194      }
195
196      if (!empty($values)) {
196      if (!empty($values)) {
197        $this->data['source'] = $values;
198      }
199    }
200  }
200  }
ContextualFormPanel->build
84  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
85    $build = parent::build($builder, $data, $options);
86
87    if (empty($build)) {
88      return $build;
91    if (self::isEmpty($build)) {
93        '#type' => 'html_tag',
103      'source' => $build,
104    ];
105
106    $build['update'] = [
107      '#type' => 'button',
108      '#value' => 'Update',
109      '#submit_button' => FALSE,
110      '#attributes' => [
111        'type' => 'button',
112        'data-wysiwyg-fix' => TRUE,
113      ],
114    ];
115
116    $build = $this->htmxEvents->onInstanceFormChange($build, $this->builderId, $this->getPluginId(), $this->data['node_id']);
117
118    return $this->htmxEvents->onInstanceUpdateButtonClick($build, $this->builderId, $this->getPluginId(), $this->data['node_id']);
119  }
ContextualFormPanel->buildForm
59  public function buildForm(array &$form, FormStateInterface $form_state): void {
60    try {
61      $contexts = $form_state->getBuildInfo()['args'][1] ?? [];
62
63      $this->alterFormValues($form_state);
64      $source = $this->sourceManager->getSource($this->data['node_id'], [], $this->data, $contexts);
65
66      if ($source instanceof SourceWithSlotsInterface) {
66      if ($source instanceof SourceWithSlotsInterface) {
67        $form = $source->settingsFormPropsOnly([], $form_state);
70        $form = $source ? $source->settingsForm([], $form_state) : [];
70        $form = $source ? $source->settingsForm([], $form_state) : [];
70        $form = $source ? $source->settingsForm([], $form_state) : [];
70        $form = $source ? $source->settingsForm([], $form_state) : [];
71      }
72
73      if ($this->isMultipleItemsSlotSource($this->data['source'])) {
73      if ($this->isMultipleItemsSlotSource($this->data['source'])) {
74        $form = $this->removeItemSelector($form);
74        $form = $this->removeItemSelector($form);
77    catch (\Exception) {
79  }
ContextualFormPanel->create
42  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
43    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
44    $instance->sourceManager = $container->get('plugin.manager.ui_patterns_source');
45
46    return $instance;
47  }
ContextualFormPanel->isApplicable
167    return isset($this->data['source_id']) && isset($this->data['node_id']);
167    return isset($this->data['source_id']) && isset($this->data['node_id']);
167    return isset($this->data['source_id']) && isset($this->data['node_id']);
168  }
ContextualFormPanel->isEmpty
211  private static function isEmpty(array $form) {
212    $keys = Element::children($form);
213
214    // Quick valid if a component. An empty component is a rare occurrence.
215    if (isset($keys['component'])) {
216      return FALSE;
219    return \array_diff(Element::children($form), [
220      'plugin_id',
221      'form_build_id',
222      'form_token',
223      'form_id',
224      // Exclude some core block with no configuration.
225      // @todo remove when we do not need the update button anymore.
226      'help_block',
227      'local_actions_block',
228      'node_syndicate_block',
229      'system_breadcrumb_block',
230      'system_clear_cache_block',
231      'system_messages_block',
232      'system_powered_by_block',
233    ]) === [];
234  }
ContextualFormPanel->isMultipleItemsSlotSource
250  private function isMultipleItemsSlotSource(array $data): bool {
251    if (!isset($data['plugin_id']) || !\is_string($data['plugin_id'])) {
251    if (!isset($data['plugin_id']) || !\is_string($data['plugin_id'])) {
251    if (!isset($data['plugin_id']) || !\is_string($data['plugin_id'])) {
252      return FALSE;
255    if (\count($data) === 1) {
257      return TRUE;
260    $plugin_id = (string) $data['plugin_id'];
261
262    if (isset($data[$plugin_id]) && \is_array($data[$plugin_id])) {
262    if (isset($data[$plugin_id]) && \is_array($data[$plugin_id])) {
262    if (isset($data[$plugin_id]) && \is_array($data[$plugin_id])) {
263      return TRUE;
266    return FALSE;
267  }
ContextualFormPanel->label
53    return 'Config';
54  }
ContextualFormPanel->onActive
138  public function onActive(string $builder_id, array $data): array {
139    return $this->reloadWithLocalData($builder_id, $data);
140  }
ContextualFormPanel->onAttachToRoot
124  public function onAttachToRoot(string $builder_id, string $instance_id): array {
125    return $this->reloadWithInstanceData($builder_id, $instance_id);
126  }
ContextualFormPanel->onAttachToSlot
131  public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array {
132    return $this->reloadWithInstanceData($builder_id, $instance_id);
133  }
ContextualFormPanel->onDelete
159  public function onDelete(string $builder_id, string $parent_id): array {
160    return $this->reloadWithLocalData($builder_id, []);
161  }
ContextualFormPanel->onUpdate
145  public function onUpdate(string $builder_id, string $instance_id): array {
146    // Reload the form itself on update.
147    // @todo pass \Drupal\display_builder\InstanceInterface object in
148    // parameters instead of loading again.
149    /** @var \Drupal\display_builder\InstanceInterface $builder */
150    $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
151    $data = $builder->getNode($instance_id);
152
153    return $this->reloadWithLocalData($builder_id, $data);
154  }
ContextualFormPanel->removeItemSelector
281  private function removeItemSelector(array $form): array {
282    $form['plugin_id']['#type'] = 'hidden';
283    unset($form['plugin_id']['#options']);
284
285    return $form;
286  }