Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 123
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Collaboration
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 15
1260
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 / 27
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 configurationSummary
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 build
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 alterRenderable
0.00% covered (danger)
0.00%
0 / 6
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
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
 onMove
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
 onHistoryChange
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 / 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
 removeInactiveUsers
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildRenderable
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
90
 rebuild
0.00% covered (danger)
0.00%
0 / 8
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\Core\Datetime\DateFormatterInterface;
8use Drupal\Core\Entity\EntityFieldManagerInterface;
9use Drupal\Core\Form\FormStateInterface;
10use Drupal\Core\Session\AccountInterface;
11use Drupal\Core\StringTranslation\TranslatableMarkup;
12use Drupal\Core\Url;
13use Drupal\display_builder\Attribute\Island;
14use Drupal\display_builder\InstanceInterface;
15use Drupal\display_builder\IslandConfigurationFormInterface;
16use Drupal\display_builder\IslandConfigurationFormTrait;
17use Drupal\display_builder\IslandPluginBase;
18use Drupal\display_builder\IslandType;
19use Drupal\file\FileInterface;
20use Symfony\Component\DependencyInjection\ContainerInterface;
21
22/**
23 * Real-time collaboration island plugin implementation.
24 */
25#[Island(
26  id: 'collaboration',
27  label: new TranslatableMarkup('Real-time collaboration'),
28  description: new TranslatableMarkup('Allow concurrent editing with multiple users.'),
29  type: IslandType::Button,
30  default_region: 'end',
31)]
32class Collaboration extends IslandPluginBase implements IslandConfigurationFormInterface {
33
34  use IslandConfigurationFormTrait;
35
36  /**
37   * Seconds in 15 minutes.
38   */
39  private const SECONDS_IN_15_MINUTES = 900;
40
41  /**
42   * The current user.
43   */
44  protected AccountInterface $currentUser;
45
46  /**
47   * The date formatter.
48   */
49  protected DateFormatterInterface $dateFormatter;
50
51  /**
52   * The entity field manager.
53   */
54  protected EntityFieldManagerInterface $entityFieldManager;
55
56  /**
57   * {@inheritdoc}
58   */
59  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
60    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
61    $instance->currentUser = $container->get('current_user');
62    $instance->dateFormatter = $container->get('date.formatter');
63    $instance->entityFieldManager = $container->get('entity_field.manager');
64
65    return $instance;
66  }
67
68  /**
69   * {@inheritdoc}
70   */
71  public function defaultConfiguration(): array {
72    return [
73      'image_field' => 'user_picture',
74      'image_style' => 'thumbnail',
75    ];
76  }
77
78  /**
79   * {@inheritdoc}
80   */
81  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
82    $options = [
83      '' => $this->t('- None -'),
84    ];
85    $configuration = $this->getConfiguration();
86
87    $fields = $this->entityFieldManager->getFieldDefinitions('user', 'user');
88
89    foreach ($fields as $field_id => $field) {
90      if ($field->getType() === 'image') {
91        $options[$field_id] = $field->getLabel();
92      }
93    }
94    $form['image_field'] = [
95      '#title' => $this->t('Image field'),
96      '#type' => 'select',
97      '#default_value' => $configuration['image_field'],
98      '#options' => $options,
99    ];
100
101    $styles = $this->entityTypeManager->getStorage('image_style')->loadMultiple();
102    $options = [
103      '' => $this->t('- None -'),
104    ];
105
106    foreach ($styles as $name => $style) {
107      $options[$name] = $style->label();
108    }
109    $form['image_style'] = [
110      '#title' => $this->t('Image style'),
111      '#type' => 'select',
112      '#default_value' => $configuration['image_style'],
113      '#options' => $options,
114    ];
115
116    return $form;
117  }
118
119  /**
120   * {@inheritdoc}
121   */
122  public function configurationSummary(): array {
123    $conf = $this->getConfiguration();
124    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
125
126    return [
127      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
128    ];
129  }
130
131  /**
132   * {@inheritdoc}
133   */
134  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
135    $builder_id = (string) $builder->id();
136    // @todo pass \Drupal\display_builder\InstanceInterface object in
137    // parameters instead of loading again.
138    /** @var \Drupal\display_builder\InstanceInterface $builder */
139    $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
140    $this->builder = $builder;
141    $users = $builder->getUsers();
142    $current_user = $this->currentUser->id();
143    $users = $this->removeInactiveUsers($users);
144
145    // If no users (this situation must not happen), don't show anything.
146    if (\count($users) === 0) {
147      return [];
148    }
149
150    if (\array_key_exists($current_user, $users)) {
151      // If the only user is the current user, don't show anything.
152      if (\count($users) === 1) {
153        return [];
154      }
155      // Move current user at the beginning of the list.
156      $users = [$current_user => $users[$current_user]] + $users;
157
158      return $this->buildRenderable($users);
159    }
160
161    // If the only user is not the current user, add they at the start.
162    if (\count($users) === 1) {
163      $users = [$current_user => NULL] + $users;
164    }
165
166    return $this->buildRenderable($users);
167  }
168
169  /**
170   * {@inheritdoc}
171   */
172  public function alterRenderable(InstanceInterface $instance, array $build): array {
173    $build['#attributes'] = [
174      'hx-ext' => 'sse',
175      'sse-connect' => Url::fromRoute('display_builder.api_sse', ['display_builder_instance' => (string) $instance->id()])->toString(),
176    ];
177    // We don't attach it from the ::build() because the island doesn't
178    // always render something in the toolbar.
179    $build['#attached']['library'][] = 'display_builder/htmx_sse';
180
181    return $build;
182  }
183
184  /**
185   * {@inheritdoc}
186   */
187  public function onAttachToRoot(string $builder_id, string $instance_id): array {
188    return $this->rebuild($builder_id);
189  }
190
191  /**
192   * {@inheritdoc}
193   */
194  public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array {
195    return $this->rebuild($builder_id);
196  }
197
198  /**
199   * {@inheritdoc}
200   */
201  public function onMove(string $builder_id, string $instance_id): array {
202    return $this->rebuild($builder_id);
203  }
204
205  /**
206   * {@inheritdoc}
207   */
208  public function onHistoryChange(string $builder_id): array {
209    return $this->rebuild($builder_id);
210  }
211
212  /**
213   * {@inheritdoc}
214   */
215  public function onUpdate(string $builder_id, string $instance_id): array {
216    return $this->rebuild($builder_id);
217  }
218
219  /**
220   * {@inheritdoc}
221   */
222  public function onDelete(string $builder_id, string $parent_id): array {
223    return $this->rebuild($builder_id);
224  }
225
226  /**
227   * Remove inactive users.
228   *
229   * @param array $users
230   *   Each key is an User entity ID, each value is a timestamp.
231   *
232   * @return array
233   *   Each key is an User entity ID, each value is a timestamp.
234   */
235  protected function removeInactiveUsers(array $users): array {
236    foreach ($users as $user_id => $time) {
237      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
238        unset($users[$user_id]);
239      }
240    }
241
242    return $users;
243  }
244
245  /**
246   * Build renderable.
247   *
248   * @param array $users
249   *   Each key is an User entity ID, each value is a timestamp.
250   *
251   * @return array
252   *   A renderable array.
253   */
254  protected function buildRenderable(array $users): array {
255    $avatars = [];
256    $configuration = $this->getConfiguration();
257
258    foreach ($users as $user_id => $time) {
259      /** @var \Drupal\user\UserInterface $user */
260      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
261
262      if (!$user) {
263        // For example, if the user was deleted.
264        continue;
265      }
266      $avatar = [
267        '#type' => 'component',
268        '#component' => 'display_builder:avatar',
269        '#props' => [
270          'name' => $user->getDisplayName(),
271        ],
272        '#attributes' => [
273          'style' => '--size: 38px',
274        ],
275      ];
276
277      if ($time) {
278        // We can't use DateFormatterInterface::formatTimeDiffSince() because
279        // the displayed value will become obsolete if the island is not updated
280        // for a while.
281        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
282        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
283      }
284
285      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
286        $image = $user->get($configuration['image_field'])->entity;
287
288        if ($image instanceof FileInterface) {
289          $image_style = $configuration['image_style'];
290          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
291          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
292        }
293      }
294      $avatars[] = $avatar;
295    }
296
297    if (\count($avatars) === 0) {
298      return [];
299    }
300
301    return [
302      '#type' => 'html_tag',
303      '#tag' => 'span',
304      '#attributes' => [
305        'class' => 'sl-avatar-group',
306      ],
307      'avatars' => $avatars,
308    ];
309  }
310
311  /**
312   * Rebuilds the island with the given builder ID.
313   *
314   * @param string $builder_id
315   *   The ID of the builder.
316   *
317   * @return array
318   *   The rebuilt island.
319   */
320  private function rebuild(string $builder_id): array {
321    if (!$this->builder) {
322      // @todo pass \Drupal\display_builder\InstanceInterface object in
323      // parameters instead of loading again.
324      /** @var \Drupal\display_builder\InstanceInterface $builder */
325      $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
326      $this->builder = $builder;
327    }
328
329    return $this->addOutOfBand(
330      $this->build($this->builder),
331      '#' . $this->getHtmlId($builder_id),
332      'innerHTML'
333    );
334  }
335
336}

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.

Collaboration->alterRenderable
172  public function alterRenderable(InstanceInterface $instance, array $build): array {
173    $build['#attributes'] = [
174      'hx-ext' => 'sse',
175      'sse-connect' => Url::fromRoute('display_builder.api_sse', ['display_builder_instance' => (string) $instance->id()])->toString(),
176    ];
177    // We don't attach it from the ::build() because the island doesn't
178    // always render something in the toolbar.
179    $build['#attached']['library'][] = 'display_builder/htmx_sse';
180
181    return $build;
182  }
Collaboration->build
134  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
135    $builder_id = (string) $builder->id();
136    // @todo pass \Drupal\display_builder\InstanceInterface object in
137    // parameters instead of loading again.
138    /** @var \Drupal\display_builder\InstanceInterface $builder */
139    $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
140    $this->builder = $builder;
141    $users = $builder->getUsers();
142    $current_user = $this->currentUser->id();
143    $users = $this->removeInactiveUsers($users);
144
145    // If no users (this situation must not happen), don't show anything.
146    if (\count($users) === 0) {
147      return [];
150    if (\array_key_exists($current_user, $users)) {
152      if (\count($users) === 1) {
153        return [];
156      $users = [$current_user => $users[$current_user]] + $users;
157
158      return $this->buildRenderable($users);
162    if (\count($users) === 1) {
163      $users = [$current_user => NULL] + $users;
164    }
165
166    return $this->buildRenderable($users);
166    return $this->buildRenderable($users);
167  }
Collaboration->buildConfigurationForm
81  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
82    $options = [
83      '' => $this->t('- None -'),
84    ];
85    $configuration = $this->getConfiguration();
86
87    $fields = $this->entityFieldManager->getFieldDefinitions('user', 'user');
88
89    foreach ($fields as $field_id => $field) {
89    foreach ($fields as $field_id => $field) {
89    foreach ($fields as $field_id => $field) {
90      if ($field->getType() === 'image') {
89    foreach ($fields as $field_id => $field) {
90      if ($field->getType() === 'image') {
91        $options[$field_id] = $field->getLabel();
89    foreach ($fields as $field_id => $field) {
89    foreach ($fields as $field_id => $field) {
90      if ($field->getType() === 'image') {
91        $options[$field_id] = $field->getLabel();
92      }
93    }
94    $form['image_field'] = [
95      '#title' => $this->t('Image field'),
96      '#type' => 'select',
97      '#default_value' => $configuration['image_field'],
98      '#options' => $options,
99    ];
100
101    $styles = $this->entityTypeManager->getStorage('image_style')->loadMultiple();
102    $options = [
103      '' => $this->t('- None -'),
104    ];
105
106    foreach ($styles as $name => $style) {
106    foreach ($styles as $name => $style) {
106    foreach ($styles as $name => $style) {
106    foreach ($styles as $name => $style) {
107      $options[$name] = $style->label();
108    }
109    $form['image_style'] = [
110      '#title' => $this->t('Image style'),
111      '#type' => 'select',
112      '#default_value' => $configuration['image_style'],
113      '#options' => $options,
114    ];
115
116    return $form;
117  }
Collaboration->buildRenderable
254  protected function buildRenderable(array $users): array {
255    $avatars = [];
256    $configuration = $this->getConfiguration();
257
258    foreach ($users as $user_id => $time) {
258    foreach ($users as $user_id => $time) {
258    foreach ($users as $user_id => $time) {
259      /** @var \Drupal\user\UserInterface $user */
260      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
261
262      if (!$user) {
264        continue;
267        '#type' => 'component',
268        '#component' => 'display_builder:avatar',
269        '#props' => [
270          'name' => $user->getDisplayName(),
271        ],
272        '#attributes' => [
273          'style' => '--size: 38px',
274        ],
275      ];
276
277      if ($time) {
281        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
282        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
283      }
284
285      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
285      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
285      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
285      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
286        $image = $user->get($configuration['image_field'])->entity;
287
288        if ($image instanceof FileInterface) {
289          $image_style = $configuration['image_style'];
290          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
291          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
291          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
291          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
291          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
292        }
293      }
294      $avatars[] = $avatar;
258    foreach ($users as $user_id => $time) {
259      /** @var \Drupal\user\UserInterface $user */
260      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
261
262      if (!$user) {
263        // For example, if the user was deleted.
264        continue;
265      }
266      $avatar = [
267        '#type' => 'component',
268        '#component' => 'display_builder:avatar',
269        '#props' => [
270          'name' => $user->getDisplayName(),
271        ],
272        '#attributes' => [
273          'style' => '--size: 38px',
274        ],
275      ];
276
277      if ($time) {
278        // We can't use DateFormatterInterface::formatTimeDiffSince() because
279        // the displayed value will become obsolete if the island is not updated
280        // for a while.
281        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
282        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
283      }
284
285      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
286        $image = $user->get($configuration['image_field'])->entity;
287
288        if ($image instanceof FileInterface) {
289          $image_style = $configuration['image_style'];
290          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
291          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
292        }
293      }
294      $avatars[] = $avatar;
258    foreach ($users as $user_id => $time) {
259      /** @var \Drupal\user\UserInterface $user */
260      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
261
262      if (!$user) {
263        // For example, if the user was deleted.
264        continue;
265      }
266      $avatar = [
267        '#type' => 'component',
268        '#component' => 'display_builder:avatar',
269        '#props' => [
270          'name' => $user->getDisplayName(),
271        ],
272        '#attributes' => [
273          'style' => '--size: 38px',
274        ],
275      ];
276
277      if ($time) {
278        // We can't use DateFormatterInterface::formatTimeDiffSince() because
279        // the displayed value will become obsolete if the island is not updated
280        // for a while.
281        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
282        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
283      }
284
285      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
286        $image = $user->get($configuration['image_field'])->entity;
287
288        if ($image instanceof FileInterface) {
289          $image_style = $configuration['image_style'];
290          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
291          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
292        }
293      }
294      $avatars[] = $avatar;
295    }
296
297    if (\count($avatars) === 0) {
298      return [];
302      '#type' => 'html_tag',
303      '#tag' => 'span',
304      '#attributes' => [
305        'class' => 'sl-avatar-group',
306      ],
307      'avatars' => $avatars,
308    ];
309  }
Collaboration->configurationSummary
123    $conf = $this->getConfiguration();
124    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
124    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
124    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
124    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
125
126    return [
127      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
127      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
127      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
127      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
128    ];
129  }
Collaboration->create
59  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
60    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
61    $instance->currentUser = $container->get('current_user');
62    $instance->dateFormatter = $container->get('date.formatter');
63    $instance->entityFieldManager = $container->get('entity_field.manager');
64
65    return $instance;
66  }
Collaboration->defaultConfiguration
73      'image_field' => 'user_picture',
74      'image_style' => 'thumbnail',
75    ];
76  }
Collaboration->onAttachToRoot
187  public function onAttachToRoot(string $builder_id, string $instance_id): array {
188    return $this->rebuild($builder_id);
189  }
Collaboration->onAttachToSlot
194  public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array {
195    return $this->rebuild($builder_id);
196  }
Collaboration->onDelete
222  public function onDelete(string $builder_id, string $parent_id): array {
223    return $this->rebuild($builder_id);
224  }
Collaboration->onHistoryChange
208  public function onHistoryChange(string $builder_id): array {
209    return $this->rebuild($builder_id);
210  }
Collaboration->onMove
201  public function onMove(string $builder_id, string $instance_id): array {
202    return $this->rebuild($builder_id);
203  }
Collaboration->onUpdate
215  public function onUpdate(string $builder_id, string $instance_id): array {
216    return $this->rebuild($builder_id);
217  }
Collaboration->rebuild
320  private function rebuild(string $builder_id): array {
321    if (!$this->builder) {
325      $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
326      $this->builder = $builder;
327    }
328
329    return $this->addOutOfBand(
329    return $this->addOutOfBand(
330      $this->build($this->builder),
331      '#' . $this->getHtmlId($builder_id),
332      'innerHTML'
333    );
334  }
Collaboration->removeInactiveUsers
235  protected function removeInactiveUsers(array $users): array {
236    foreach ($users as $user_id => $time) {
236    foreach ($users as $user_id => $time) {
236    foreach ($users as $user_id => $time) {
237      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
236    foreach ($users as $user_id => $time) {
237      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
238        unset($users[$user_id]);
236    foreach ($users as $user_id => $time) {
236    foreach ($users as $user_id => $time) {
237      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
238        unset($users[$user_id]);
239      }
240    }
241
242    return $users;
243  }