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