Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
80.39% covered (warning)
80.39%
41 / 51
51.28% covered (warning)
51.28%
20 / 39
16.67% covered (danger)
16.67%
5 / 30
60.00% covered (warning)
60.00%
9 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
DisplayBuilderEventsSubscriber
80.39% covered (warning)
80.39%
41 / 51
51.28% covered (warning)
51.28%
20 / 39
16.67% covered (danger)
16.67%
5 / 30
60.00% covered (warning)
60.00%
9 / 15
417.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
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
 getSubscribedEvents
100.00% covered (success)
100.00%
13 / 13
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
 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
 onAttachToRoot
100.00% covered (success)
100.00%
1 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onAttachToSlot
100.00% covered (success)
100.00%
1 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onDelete
100.00% covered (success)
100.00%
1 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onHistoryChange
100.00% covered (success)
100.00%
1 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onRestore
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
 onRevert
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
 onMove
100.00% covered (success)
100.00%
1 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onUpdate
100.00% covered (success)
100.00%
1 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
100.00% covered (success)
100.00%
1 / 1
1
 onPublish
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
 onPresetSave
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
 dispatchToIslands
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
14 / 14
10.00% covered (danger)
10.00%
1 / 10
100.00% covered (success)
100.00%
1 / 1
42.72
 shouldDefer
50.00% covered (danger)
50.00%
5 / 10
33.33% covered (danger)
33.33%
4 / 12
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
19.12
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Event;
6
7use Drupal\display_builder\Island\IslandInterface;
8use Drupal\display_builder\Island\IslandPluginManagerInterface;
9use Drupal\display_builder\Island\IslandType;
10use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11
12/**
13 * The event subscriber for Display Builder islands.
14 */
15class DisplayBuilderEventsSubscriber implements EventSubscriberInterface {
16
17  /**
18   * Constructs a new DisplayBuilderEventsSubscriber object.
19   */
20  public function __construct(
21    protected IslandPluginManagerInterface $islandManager,
22  ) {}
23
24  /**
25   * {@inheritdoc}
26   */
27  public static function getSubscribedEvents(): array {
28    return [
29      DisplayBuilderEvents::ON_ACTIVE => 'onActive',
30      DisplayBuilderEvents::ON_ATTACH_TO_ROOT => 'onAttachToRoot',
31      DisplayBuilderEvents::ON_ATTACH_TO_SLOT => 'onAttachToSlot',
32      DisplayBuilderEvents::ON_DELETE => 'onDelete',
33      DisplayBuilderEvents::ON_HISTORY_CHANGE => 'onHistoryChange',
34      DisplayBuilderEvents::ON_RESTORE => 'onRestore',
35      DisplayBuilderEvents::ON_REVERT => 'onRevert',
36      DisplayBuilderEvents::ON_MOVE => 'onMove',
37      DisplayBuilderEvents::ON_UPDATE => 'onUpdate',
38      DisplayBuilderEvents::ON_PUBLISH => 'onPublish',
39      DisplayBuilderEvents::ON_PRESET_SAVE => 'onPresetSave',
40    ];
41  }
42
43  /**
44   * Event handler for when a block becomes active.
45   *
46   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
47   *   The event object.
48   */
49  public function onActive(DisplayBuilderEvent $event): void {
50    $this->dispatchToIslands($event, __FUNCTION__, [$event->getData()]);
51  }
52
53  /**
54   * Event handler for when a block is attached to the root.
55   *
56   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
57   *   The event object.
58   */
59  public function onAttachToRoot(DisplayBuilderEvent $event): void {
60    $this->dispatchToIslands($event, __FUNCTION__, [$event->getNodeId()]);
61  }
62
63  /**
64   * Event handler for when a block is attached to a slot.
65   *
66   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
67   *   The event object.
68   */
69  public function onAttachToSlot(DisplayBuilderEvent $event): void {
70    $this->dispatchToIslands($event, __FUNCTION__, [$event->getNodeId(), $event->getParentId()]);
71  }
72
73  /**
74   * Event handler for when a block is deleted.
75   *
76   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
77   *   The event object.
78   */
79  public function onDelete(DisplayBuilderEvent $event): void {
80    $this->dispatchToIslands($event, __FUNCTION__, [$event->getParentId()]);
81  }
82
83  /**
84   * Event handler for when the history changes.
85   *
86   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
87   *   The event object.
88   */
89  public function onHistoryChange(DisplayBuilderEvent $event): void {
90    $this->dispatchToIslands($event, __FUNCTION__);
91  }
92
93  /**
94   * Event handler for when the builder is restored to its last saved state.
95   *
96   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
97   *   The event object.
98   */
99  public function onRestore(DisplayBuilderEvent $event): void {
100    $this->dispatchToIslands($event, __FUNCTION__);
101  }
102
103  /**
104   * Event handler for when an override is reverted to its default state.
105   *
106   * Reuses the history-change island callbacks since the UI refresh is
107   * identical: all islands must re-render the updated state and history.
108   *
109   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
110   *   The event object.
111   */
112  public function onRevert(DisplayBuilderEvent $event): void {
113    $this->dispatchToIslands($event, __FUNCTION__);
114  }
115
116  /**
117   * Event handler for when a block is moved.
118   *
119   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
120   *   The event object.
121   */
122  public function onMove(DisplayBuilderEvent $event): void {
123    $this->dispatchToIslands($event, __FUNCTION__, [$event->getNodeId()]);
124  }
125
126  /**
127   * Event handler for when a block is updated.
128   *
129   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
130   *   The event object.
131   */
132  public function onUpdate(DisplayBuilderEvent $event): void {
133    $this->dispatchToIslands($event, __FUNCTION__, [$event->getNodeId()]);
134  }
135
136  /**
137   * Event handler for when a display is saved.
138   *
139   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
140   *   The event object.
141   */
142  public function onPublish(DisplayBuilderEvent $event): void {
143    $this->dispatchToIslands($event, __FUNCTION__, [$event->getData()]);
144  }
145
146  /**
147   * Event handler for when a preset is saved.
148   *
149   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
150   *   The event object.
151   */
152  public function onPresetSave(DisplayBuilderEvent $event): void {
153    $this->dispatchToIslands($event, __FUNCTION__);
154  }
155
156  /**
157   * Dispatch the event with a generic code.
158   *
159   * @param \Drupal\display_builder\Event\DisplayBuilderEvent $event
160   *   The event object.
161   * @param string $method
162   *   The method to dispatch.
163   * @param array $parameters
164   *   (Optional) The parameters to the method.
165   */
166  private function dispatchToIslands(DisplayBuilderEvent $event, string $method, array $parameters = []): void {
167    \array_unshift($parameters, $event->getInstance());
168
169    $configuration = $event->getIslandConfiguration();
170    $contexts = $event->getInstance()->getAvailableContexts();
171    $islands = $this->islandManager->createInstances($this->islandManager->getDefinitions(), $contexts, $configuration);
172
173    $island_enabled = $event->getEnabledIslands();
174    $visible_islands = $event->getVisibleIslands();
175
176    foreach ($islands as $island_id => $island) {
177      if (!isset($island_enabled[$island_id])) {
178        continue;
179      }
180
181      // Skip the island triggering the HTMX event. Useful to avoid swapping
182      // the content of an island which is already in the expected state.
183      // For examples, if we move an instance in Builder, Wireframe or Tree
184      // panels, if we change the settings in InstanceForm.
185      // @see Drupal\display_builder\Controller\ApiControllerBase::islandId
186      if ($island_id === $event->getCurrentIslandId()) {
187        continue;
188      }
189
190      // Skip panels the client told us are off screen. They are rebuilt on
191      // demand when the user brings them back into view.
192      if ($visible_islands !== NULL && $this->shouldDefer($island, $visible_islands)) {
193        continue;
194      }
195
196      $result = $island->{$method}(...$parameters);
197
198      if ($result !== NULL) {
199        $event->appendResult($island_id, $result);
200      }
201    }
202  }
203
204  /**
205   * Determines whether an island's rebuild can be deferred until it is shown.
206   *
207   * Only panels the user cannot currently see are worth deferring, and only
208   * where the client is able to notice they went stale and ask for them again.
209   * That is true of View panels in the tabbed main area and in the start
210   * sidebar drawer - one visible at a time in each - and of the Floating
211   * controls which are shown and hidden along with the panel they attach to.
212   *
213   * Everything else (toolbar buttons, contextual menu entries, Library and
214   * Contextual tabs) is always rendered: it is either permanently on screen or
215   * cheap enough that deferring it would cost more than it saves.
216   *
217   * Which islands are eligible at all is the island's own answer, so a panel
218   * that cannot survive a standalone reload can decline. This method only
219   * decides whether an eligible island is currently off screen.
220   *
221   * The region is deliberately not consulted: IslandType::regions() offers a
222   * View island only 'main' and 'sidebar', both of which show one panel at a
223   * time, so every View island qualifies. Testing the region would also be
224   * wrong, since it is only stored on the profile when explicitly configured
225   * (@see \Drupal\display_builder\Entity\Profile::setIslandConfiguration()) -
226   * an island left at its default would fall through and never be deferred.
227   *
228   * @param \Drupal\display_builder\Island\IslandInterface $island
229   *   The island to test.
230   * @param array $visible_islands
231   *   The island plugin IDs the client reports as currently visible.
232   *
233   * @return bool
234   *   TRUE if this island's rebuild should be skipped for this request.
235   *
236   * @see \Drupal\display_builder\Island\IslandInterface::isDeferrable()
237   * @see \Drupal\display_builder\Controller\ApiController::reloadIsland()
238   */
239  private function shouldDefer(IslandInterface $island, array $visible_islands): bool {
240    if (!$island->isDeferrable()) {
241      return FALSE;
242    }
243
244    $type = $island->getTypeId();
245
246    if ($type === IslandType::View->value) {
247      return !\in_array($island->getPluginId(), $visible_islands, TRUE);
248    }
249
250    // A Floating island rides along with the panel(s) it is attached to, so it
251    // is off screen exactly when all of them are.
252    // @see \Drupal\display_builder\ProfileViewBuilder::buildFloatingControlsRegion()
253    if ($type === IslandType::Floating->value) {
254      $definition = $island->getPluginDefinition();
255      $attach_to = \is_array($definition) ? ($definition['attach_to'] ?? []) : [];
256
257      return $attach_to !== [] && \array_intersect($attach_to, $visible_islands) === [];
258    }
259
260    return FALSE;
261  }
262
263}