Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 162
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ViewportSwitcher
0.00% covered (danger)
0.00%
0 / 157
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 9
756
0.00% covered (danger)
0.00%
0 / 1
 create
0.00% covered (danger)
0.00%
0 / 5
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
 defaultConfiguration
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
 buildConfigurationForm
0.00% covered (danger)
0.00%
0 / 18
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
 configurationSummary
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 build
0.00% covered (danger)
0.00%
0 / 82
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
56
 getDefinitions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getMaxWidthValueFromMediaQuery
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 getProvidersOptions
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getProviders
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\breakpoint\BreakpointManager;
8use Drupal\Core\Extension\ModuleExtensionList;
9use Drupal\Core\Extension\ThemeExtensionList;
10use Drupal\Core\Form\FormStateInterface;
11use Drupal\Core\StringTranslation\TranslatableMarkup;
12use Drupal\display_builder\Attribute\Island;
13use Drupal\display_builder\InstanceInterface;
14use Drupal\display_builder\IslandConfigurationFormInterface;
15use Drupal\display_builder\IslandConfigurationFormTrait;
16use Drupal\display_builder\IslandPluginBase;
17use Drupal\display_builder\IslandType;
18use Symfony\Component\DependencyInjection\ContainerInterface;
19
20/**
21 * Island plugin implementation.
22 */
23#[Island(
24  id: 'viewport',
25  label: new TranslatableMarkup('Viewport switcher'),
26  description: new TranslatableMarkup('Change main region width according to breakpoints.'),
27  type: IslandType::Button,
28)]
29class ViewportSwitcher extends IslandPluginBase implements IslandConfigurationFormInterface {
30
31  use IslandConfigurationFormTrait;
32
33  private const HIDE_PROVIDER = ['toolbar'];
34
35  /**
36   * The module list extension service.
37   */
38  protected ThemeExtensionList $themeList;
39
40  /**
41   * The module list extension service.
42   */
43  protected ModuleExtensionList $moduleList;
44
45  /**
46   * The breakpoint manager.
47   */
48  protected BreakpointManager $breakpointManager;
49
50  /**
51   * {@inheritdoc}
52   */
53  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
54    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
55    $instance->themeList = $container->get('extension.list.theme');
56    $instance->moduleList = $container->get('extension.list.module');
57    $instance->breakpointManager = $container->get('breakpoint.manager');
58
59    return $instance;
60  }
61
62  /**
63   * {@inheritdoc}
64   */
65  public function defaultConfiguration(): array {
66    return [
67      'exclude' => [],
68      'format' => 'default',
69    ];
70  }
71
72  /**
73   * {@inheritdoc}
74   */
75  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
76    $configuration = $this->getConfiguration();
77
78    $form['format'] = [
79      '#type' => 'select',
80      '#title' => $this->t('Selector format'),
81      '#description' => $this->t('Choose the appearance of the selector. Normal select or a compact dropdown menu.'),
82      '#options' => [
83        'default' => $this->t('Default'),
84        'compact' => $this->t('Compact'),
85      ],
86      '#default_value' => $configuration['format'],
87    ];
88
89    $form['exclude'] = [
90      '#type' => 'checkboxes',
91      '#title' => $this->t('Exclude providers'),
92      '#options' => $this->getProvidersOptions($this->t('breakpoint'), $this->t('breakpoints')),
93      '#default_value' => $configuration['exclude'],
94    ];
95
96    return $form;
97  }
98
99  /**
100   * {@inheritdoc}
101   */
102  public function configurationSummary(): array {
103    $configuration = $this->getConfiguration();
104    $summary = [];
105
106    $exclude = \array_filter($configuration['exclude'] ?? []);
107
108    $summary[] = $this->t('Excluded providers: @exclude', [
109      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
110    ]);
111
112    $summary[] = $this->t('Format: @format', [
113      '@format' => $configuration['format'] ?? 'default',
114    ]);
115
116    return $summary;
117  }
118
119  /**
120   * {@inheritdoc}
121   */
122  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
123    $configuration = $this->getConfiguration();
124    $definitions = $this->getDefinitions();
125
126    $groups = [];
127
128    foreach ($definitions as $definition) {
129      if (!isset($definition['group'])) {
130        continue;
131      }
132      $groups[$definition['group']] = $definition['group'];
133    }
134
135    $options = $data = [];
136    $items = [
137      [
138        'title' => $this->t('Fluid'),
139        'class' => 'active',
140      ],
141      [
142        'divider' => TRUE,
143      ],
144    ];
145
146    foreach ($groups as $group_id => $label) {
147      $points = $this->breakpointManager->getBreakpointsByGroup($group_id);
148
149      foreach ($points as $point_id => $point) {
150        $query = $point->getMediaQuery();
151        $width = $this->getMaxWidthValueFromMediaQuery($query);
152
153        if (!$width) {
154          continue;
155        }
156        $point_label = $point->getLabel();
157        $options[$label][$point_id] = $point_label;
158        $items[] = [
159          'title' => $point_label,
160          'value' => $point_id,
161        ];
162        $data[$point_id] = $width;
163      }
164    }
165
166    $select = [
167      '#type' => 'component',
168      '#component' => 'display_builder:select',
169      '#attached' => [
170        'library' => ['display_builder/viewport_switcher'],
171      ],
172      '#props' => [
173        'options' => $options,
174        'icon' => 'window',
175        'empty_option' => $this->t('Fluid'),
176      ],
177      '#attributes' => [
178        'style' => 'display: inline-block;',
179        'data-points' => \json_encode($data),
180        'data-island-action' => 'viewport',
181      ],
182    ];
183
184    if ($configuration['format'] !== 'compact') {
185      return $select;
186    }
187
188    unset($select['#attributes']['style']);
189    $select['#props']['icon'] = NULL;
190
191    $button = $this->buildButton('', NULL, 'display', $this->t('Switch viewport of this display'));
192    $button['#attributes']['class'] = ['switch-viewport-btn'];
193
194    return [
195      '#type' => 'component',
196      '#component' => 'display_builder:dropdown',
197      '#slots' => [
198        'button' => $button,
199        'content' => [
200          '#type' => 'component',
201          '#component' => 'display_builder:menu',
202          '#props' => [
203            'items' => $items,
204          ],
205          '#attributes' => [
206            'class' => ['viewport-menu', 'db-background'],
207            'data-points' => \json_encode($data),
208          ],
209        ],
210      ],
211      '#props' => [
212        'tooltip' => $this->t('Switch viewport'),
213      ],
214      '#attributes' => [
215        'class' => ['switch-viewport'],
216        'data-island-action' => 'viewport',
217      ],
218      '#attached' => [
219        'library' => ['display_builder/viewport_switcher'],
220      ],
221    ];
222  }
223
224  /**
225   * Get the definitions list which is used by a few methods.
226   *
227   * @return array
228   *   Breakpoints plugin definitions as associative arrays.
229   */
230  public function getDefinitions(): array {
231    $definitions = $this->breakpointManager->getDefinitions();
232
233    foreach ($definitions as $definition_id => $definition) {
234      if (isset($definition['provider']) && \in_array($definition['provider'], self::HIDE_PROVIDER, TRUE)) {
235        unset($definitions[$definition_id]);
236      }
237
238      // Exclude definitions with not supported media queries.
239      if (!$this->getMaxWidthValueFromMediaQuery($definition['mediaQuery'])) {
240        unset($definitions[$definition_id]);
241      }
242    }
243
244    return $definitions;
245  }
246
247  /**
248   * Get width and max width from media query.
249   *
250   * @param string $query
251   *   The media query from the breakpoint definition.
252   *
253   * @return ?string
254   *   The width with its unit (120px, 13em, 100vh...). Null is no width found.
255   */
256  protected function getMaxWidthValueFromMediaQuery(string $query): ?string {
257    if (\str_contains($query, 'not ')) {
258      // Queries with negated expression(s) are not supported.
259      return NULL;
260    }
261    // Look for max-width: 1250px or max-width: 1250 px.
262    \preg_match('/max-width:\s*([0-9]+)\s*([A-Za-z]+)/', $query, $matches);
263
264    if (\count($matches) > 2) {
265      return $matches[1] . $matches[2];
266    }
267    // Looking for width <= 1250px or <= 1250 px.
268    \preg_match('/width\s*<=\s*([0-9]+)\s*([A-Za-z]+)/', $query, $matches);
269
270    if (\count($matches) > 1) {
271      return $matches[1];
272    }
273
274    // @todo Currently only supports queries with max-width or width <= using
275    // px, em, vh units.
276    // Does not support min-width, percentage units, or complex/combined media
277    // queries.
278    return NULL;
279  }
280
281  /**
282   * Get providers options for select input.
283   *
284   * @param string|TranslatableMarkup $singular
285   *   Singular label of the plugins.
286   * @param string|TranslatableMarkup $plural
287   *   Plural label of the plugins.
288   *
289   * @return array
290   *   An associative array with extension ID as key and extension description
291   *   as value.
292   */
293  protected function getProvidersOptions(string|TranslatableMarkup $singular = 'definition', string|TranslatableMarkup $plural = 'definitions'): array {
294    $options = [];
295
296    foreach ($this->getProviders($this->getDefinitions()) as $provider_id => $provider) {
297      $params = [
298        '@name' => $provider['name'],
299        '@type' => $provider['type'],
300        '@count' => $provider['count'],
301        '@singular' => $singular,
302        '@plural' => $plural,
303      ];
304      $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@type, @count @singular)', '@name (@type, @count @plural)', $params);
305    }
306
307    return $options;
308  }
309
310  /**
311   * Get all providers.
312   *
313   * @param array $definitions
314   *   Plugin definitions.
315   *
316   * @return array
317   *   Drupal extension definitions, keyed by extension ID
318   */
319  protected function getProviders(array $definitions): array {
320    $themes = $this->themeList->getAllInstalledInfo();
321    $modules = $this->moduleList->getAllInstalledInfo();
322    $providers = [];
323
324    foreach ($definitions as $definition) {
325      $provider_id = $definition['provider'];
326      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
327
328      if (!$provider) {
329        continue;
330      }
331      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
332      $providers[$provider_id] = $provider;
333    }
334
335    return $providers;
336  }
337
338}

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.

ViewportSwitcher->build
122  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
123    $configuration = $this->getConfiguration();
124    $definitions = $this->getDefinitions();
125
126    $groups = [];
127
128    foreach ($definitions as $definition) {
128    foreach ($definitions as $definition) {
129      if (!isset($definition['group'])) {
130        continue;
128    foreach ($definitions as $definition) {
129      if (!isset($definition['group'])) {
130        continue;
131      }
132      $groups[$definition['group']] = $definition['group'];
128    foreach ($definitions as $definition) {
129      if (!isset($definition['group'])) {
130        continue;
131      }
132      $groups[$definition['group']] = $definition['group'];
133    }
134
135    $options = $data = [];
136    $items = [
137      [
138        'title' => $this->t('Fluid'),
139        'class' => 'active',
140      ],
141      [
142        'divider' => TRUE,
143      ],
144    ];
145
146    foreach ($groups as $group_id => $label) {
146    foreach ($groups as $group_id => $label) {
146    foreach ($groups as $group_id => $label) {
147      $points = $this->breakpointManager->getBreakpointsByGroup($group_id);
148
149      foreach ($points as $point_id => $point) {
149      foreach ($points as $point_id => $point) {
149      foreach ($points as $point_id => $point) {
150        $query = $point->getMediaQuery();
151        $width = $this->getMaxWidthValueFromMediaQuery($query);
152
153        if (!$width) {
154          continue;
149      foreach ($points as $point_id => $point) {
150        $query = $point->getMediaQuery();
151        $width = $this->getMaxWidthValueFromMediaQuery($query);
152
153        if (!$width) {
154          continue;
155        }
156        $point_label = $point->getLabel();
146    foreach ($groups as $group_id => $label) {
147      $points = $this->breakpointManager->getBreakpointsByGroup($group_id);
148
149      foreach ($points as $point_id => $point) {
146    foreach ($groups as $group_id => $label) {
147      $points = $this->breakpointManager->getBreakpointsByGroup($group_id);
148
149      foreach ($points as $point_id => $point) {
150        $query = $point->getMediaQuery();
151        $width = $this->getMaxWidthValueFromMediaQuery($query);
152
153        if (!$width) {
154          continue;
155        }
156        $point_label = $point->getLabel();
157        $options[$label][$point_id] = $point_label;
158        $items[] = [
159          'title' => $point_label,
160          'value' => $point_id,
161        ];
162        $data[$point_id] = $width;
163      }
164    }
165
166    $select = [
167      '#type' => 'component',
168      '#component' => 'display_builder:select',
169      '#attached' => [
170        'library' => ['display_builder/viewport_switcher'],
171      ],
172      '#props' => [
173        'options' => $options,
174        'icon' => 'window',
175        'empty_option' => $this->t('Fluid'),
176      ],
177      '#attributes' => [
178        'style' => 'display: inline-block;',
179        'data-points' => \json_encode($data),
180        'data-island-action' => 'viewport',
181      ],
182    ];
183
184    if ($configuration['format'] !== 'compact') {
185      return $select;
188    unset($select['#attributes']['style']);
189    $select['#props']['icon'] = NULL;
190
191    $button = $this->buildButton('', NULL, 'display', $this->t('Switch viewport of this display'));
192    $button['#attributes']['class'] = ['switch-viewport-btn'];
193
194    return [
195      '#type' => 'component',
ViewportSwitcher->buildConfigurationForm
75  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
76    $configuration = $this->getConfiguration();
77
78    $form['format'] = [
79      '#type' => 'select',
80      '#title' => $this->t('Selector format'),
81      '#description' => $this->t('Choose the appearance of the selector. Normal select or a compact dropdown menu.'),
82      '#options' => [
83        'default' => $this->t('Default'),
84        'compact' => $this->t('Compact'),
85      ],
86      '#default_value' => $configuration['format'],
87    ];
88
89    $form['exclude'] = [
90      '#type' => 'checkboxes',
91      '#title' => $this->t('Exclude providers'),
92      '#options' => $this->getProvidersOptions($this->t('breakpoint'), $this->t('breakpoints')),
93      '#default_value' => $configuration['exclude'],
94    ];
95
96    return $form;
ViewportSwitcher->configurationSummary
103    $configuration = $this->getConfiguration();
104    $summary = [];
105
106    $exclude = \array_filter($configuration['exclude'] ?? []);
107
108    $summary[] = $this->t('Excluded providers: @exclude', [
109      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
109      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
109      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
109      '@exclude' => ($exclude = \array_filter($configuration['exclude'] ?? [])) ? \implode(', ', $exclude) : $this->t('None'),
110    ]);
111
112    $summary[] = $this->t('Format: @format', [
113      '@format' => $configuration['format'] ?? 'default',
114    ]);
115
116    return $summary;
ViewportSwitcher->create
53  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
54    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
55    $instance->themeList = $container->get('extension.list.theme');
56    $instance->moduleList = $container->get('extension.list.module');
57    $instance->breakpointManager = $container->get('breakpoint.manager');
58
59    return $instance;
ViewportSwitcher->defaultConfiguration
67      'exclude' => [],
ViewportSwitcher->getDefinitions
231    $definitions = $this->breakpointManager->getDefinitions();
232
233    foreach ($definitions as $definition_id => $definition) {
233    foreach ($definitions as $definition_id => $definition) {
233    foreach ($definitions as $definition_id => $definition) {
234      if (isset($definition['provider']) && \in_array($definition['provider'], self::HIDE_PROVIDER, TRUE)) {
234      if (isset($definition['provider']) && \in_array($definition['provider'], self::HIDE_PROVIDER, TRUE)) {
235        unset($definitions[$definition_id]);
236      }
237
238      // Exclude definitions with not supported media queries.
239      if (!$this->getMaxWidthValueFromMediaQuery($definition['mediaQuery'])) {
239      if (!$this->getMaxWidthValueFromMediaQuery($definition['mediaQuery'])) {
233    foreach ($definitions as $definition_id => $definition) {
234      if (isset($definition['provider']) && \in_array($definition['provider'], self::HIDE_PROVIDER, TRUE)) {
235        unset($definitions[$definition_id]);
236      }
237
238      // Exclude definitions with not supported media queries.
239      if (!$this->getMaxWidthValueFromMediaQuery($definition['mediaQuery'])) {
240        unset($definitions[$definition_id]);
233    foreach ($definitions as $definition_id => $definition) {
234      if (isset($definition['provider']) && \in_array($definition['provider'], self::HIDE_PROVIDER, TRUE)) {
235        unset($definitions[$definition_id]);
236      }
237
238      // Exclude definitions with not supported media queries.
239      if (!$this->getMaxWidthValueFromMediaQuery($definition['mediaQuery'])) {
240        unset($definitions[$definition_id]);
241      }
242    }
243
244    return $definitions;
ViewportSwitcher->getMaxWidthValueFromMediaQuery
256  protected function getMaxWidthValueFromMediaQuery(string $query): ?string {
257    if (\str_contains($query, 'not ')) {
259      return NULL;
262    \preg_match('/max-width:\s*([0-9]+)\s*([A-Za-z]+)/', $query, $matches);
263
264    if (\count($matches) > 2) {
265      return $matches[1] . $matches[2];
268    \preg_match('/width\s*<=\s*([0-9]+)\s*([A-Za-z]+)/', $query, $matches);
269
270    if (\count($matches) > 1) {
271      return $matches[1];
278    return NULL;
ViewportSwitcher->getProviders
319  protected function getProviders(array $definitions): array {
320    $themes = $this->themeList->getAllInstalledInfo();
321    $modules = $this->moduleList->getAllInstalledInfo();
322    $providers = [];
323
324    foreach ($definitions as $definition) {
324    foreach ($definitions as $definition) {
325      $provider_id = $definition['provider'];
326      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
327
328      if (!$provider) {
329        continue;
331      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
331      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
331      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
324    foreach ($definitions as $definition) {
325      $provider_id = $definition['provider'];
326      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
327
328      if (!$provider) {
329        continue;
330      }
331      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
324    foreach ($definitions as $definition) {
325      $provider_id = $definition['provider'];
326      $provider = $themes[$provider_id] ?? $modules[$provider_id] ?? NULL;
327
328      if (!$provider) {
329        continue;
330      }
331      $provider['count'] = isset($providers[$provider_id]) ? ($providers[$provider_id]['count']) + 1 : 1;
332      $providers[$provider_id] = $provider;
333    }
334
335    return $providers;
ViewportSwitcher->getProvidersOptions
293  protected function getProvidersOptions(string|TranslatableMarkup $singular = 'definition', string|TranslatableMarkup $plural = 'definitions'): array {
294    $options = [];
295
296    foreach ($this->getProviders($this->getDefinitions()) as $provider_id => $provider) {
296    foreach ($this->getProviders($this->getDefinitions()) as $provider_id => $provider) {
296    foreach ($this->getProviders($this->getDefinitions()) as $provider_id => $provider) {
296    foreach ($this->getProviders($this->getDefinitions()) as $provider_id => $provider) {
297      $params = [
298        '@name' => $provider['name'],
299        '@type' => $provider['type'],
300        '@count' => $provider['count'],
301        '@singular' => $singular,
302        '@plural' => $plural,
303      ];
304      $options[$provider_id] = $this->formatPlural($provider['count'], '@name (@type, @count @singular)', '@name (@type, @count @plural)', $params);
305    }
306
307    return $options;