Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
9.57% |
27 / 282 |
|
25.00% |
3 / 12 |
CRAP | |
0.00% |
0 / 1 |
VisitorsDisplayLink | |
9.57% |
27 / 282 |
|
25.00% |
3 / 12 |
2904.21 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
create | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
defineOptions | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
buildOptionsForm | |
0.00% |
0 / 86 |
|
0.00% |
0 / 1 |
156 | |||
removeDisplayLink | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
validate | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
240 | |||
render | |
56.00% |
14 / 25 |
|
0.00% |
0 / 1 |
37.81 | |||
renderSingleLink | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
42 | |||
renderRadioButtons | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
renderCheckboxes | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
renderUnorderedList | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
ajaxDisplayChange | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Drupal\visitors\Plugin\views\area; |
4 | |
5 | use Drupal\Core\Config\ImmutableConfig; |
6 | use Drupal\Core\EventSubscriber\AjaxResponseSubscriber; |
7 | use Drupal\Core\EventSubscriber\MainContentViewSubscriber; |
8 | use Drupal\Core\Form\FormBuilderInterface; |
9 | use Drupal\Core\Form\FormStateInterface; |
10 | use Drupal\Core\Url; |
11 | use Drupal\views\Attribute\ViewsArea; |
12 | use Drupal\views\Plugin\views\area\DisplayLink; |
13 | use Symfony\Component\DependencyInjection\ContainerInterface; |
14 | |
15 | /** |
16 | * Views area display_link handler. |
17 | * |
18 | * @ingroup views_area_handlers |
19 | */ |
20 | #[ViewsArea("visitors_display_link")] |
21 | class VisitorsDisplayLink extends DisplayLink { |
22 | |
23 | /** |
24 | * The view settings. |
25 | * |
26 | * @var \Drupal\Core\Config\ImmutableConfig |
27 | */ |
28 | protected $viewSettings; |
29 | |
30 | /** |
31 | * Constructs a new VisitorsDisplayLink object. |
32 | * |
33 | * @param array $configuration |
34 | * The plugin configuration. |
35 | * @param string $plugin_id |
36 | * The plugin ID. |
37 | * @param mixed $plugin_definition |
38 | * The plugin definition. |
39 | * @param \Drupal\Core\Config\ImmutableConfig $view_settings |
40 | * The view settings. |
41 | */ |
42 | public function __construct(array $configuration, $plugin_id, $plugin_definition, ImmutableConfig $view_settings) { |
43 | parent::__construct($configuration, $plugin_id, $plugin_definition); |
44 | $this->viewSettings = $view_settings; |
45 | } |
46 | |
47 | /** |
48 | * {@inheritdoc} |
49 | */ |
50 | public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { |
51 | return new self( |
52 | $configuration, |
53 | $plugin_id, |
54 | $plugin_definition, |
55 | $container->get('config.factory')->get('views.settings') |
56 | ); |
57 | } |
58 | |
59 | /** |
60 | * {@inheritdoc} |
61 | */ |
62 | protected function defineOptions() { |
63 | $options = parent::defineOptions(); |
64 | $options['display_links'] = ['default' => []]; |
65 | $options['bidirectional_label'] = ['default' => '']; |
66 | $options['render_format'] = ['default' => 'link']; |
67 | return $options; |
68 | } |
69 | |
70 | /** |
71 | * {@inheritdoc} |
72 | */ |
73 | public function buildOptionsForm(&$form, FormStateInterface $form_state) { |
74 | parent::buildOptionsForm($form, $form_state); |
75 | |
76 | $allowed_displays = []; |
77 | $displays = $this->view->storage->get('display'); |
78 | foreach ($displays as $display_id => $display) { |
79 | if ($this->isPathBasedDisplay($display_id)) { |
80 | unset($displays[$display_id]); |
81 | continue; |
82 | } |
83 | $allowed_displays[$display_id] = $display['display_title']; |
84 | } |
85 | |
86 | $form['description'] = [ |
87 | [ |
88 | '#markup' => $this->t('To make sure the results are the same when switching to the other display, it is recommended to make sure the display:'), |
89 | ], |
90 | [ |
91 | '#theme' => 'item_list', |
92 | '#items' => [ |
93 | $this->t('Has a path.'), |
94 | $this->t('Has the same filter criteria.'), |
95 | $this->t('Has the same sort criteria.'), |
96 | $this->t('Has the same contextual filters.'), |
97 | ], |
98 | ], |
99 | ]; |
100 | |
101 | if (!$allowed_displays) { |
102 | $form['empty_message'] = [ |
103 | '#markup' => '<p><em>' . $this->t('There are no path-based displays available.') . '</em></p>', |
104 | ]; |
105 | } |
106 | else { |
107 | // Render format selection. |
108 | $form['render_format'] = [ |
109 | '#title' => $this->t('Render format'), |
110 | '#type' => 'select', |
111 | '#options' => [ |
112 | 'link' => $this->t('Single link (if only one display)'), |
113 | 'radio' => $this->t('Radio buttons'), |
114 | 'checkboxes' => $this->t('Checkboxes'), |
115 | 'list' => $this->t('Unordered list of links'), |
116 | ], |
117 | '#default_value' => (string) ($this->options['render_format'] ?? 'link'), |
118 | '#description' => $this->t('Choose how to render the display links.'), |
119 | ]; |
120 | |
121 | // Bidirectional label configuration. |
122 | $form['bidirectional_label'] = [ |
123 | '#title' => $this->t('Bidirectional link label'), |
124 | '#description' => $this->t('If the target displays have VisitorsDisplayLink areas with this exact label, the links will be overridden to point back to this display. Leave empty to disable bidirectional linking.'), |
125 | '#type' => 'textfield', |
126 | '#default_value' => (string) ($this->options['bidirectional_label'] ?? ''), |
127 | ]; |
128 | |
129 | // Display links configuration. |
130 | $form['display_links'] = [ |
131 | '#type' => 'fieldset', |
132 | '#title' => $this->t('Display Links'), |
133 | '#description' => $this->t('Configure multiple display links. Each link can have its own label.'), |
134 | '#tree' => TRUE, |
135 | ]; |
136 | |
137 | $display_links = $this->options['display_links'] ?? []; |
138 | if (empty($display_links)) { |
139 | // Initialize with one empty link if none exist. |
140 | $display_links = [['display_id' => '', 'label' => '']]; |
141 | } |
142 | else { |
143 | // Always add an empty link at the end for adding new ones. |
144 | $display_links[] = ['display_id' => '', 'label' => '']; |
145 | } |
146 | |
147 | $total_links = count($display_links); |
148 | foreach ($display_links as $index => $link) { |
149 | // Ensure we have a valid array structure. |
150 | if (!is_array($link)) { |
151 | continue; |
152 | } |
153 | |
154 | $is_empty = empty((string) ($link['display_id'] ?? '')) && empty((string) ($link['label'] ?? '')); |
155 | $is_last = ($index === $total_links - 1); |
156 | |
157 | $form['display_links'][$index] = [ |
158 | '#type' => 'fieldset', |
159 | '#title' => $is_empty ? $this->t('Add new link') : $this->t('Link @index', ['@index' => (int) $index + 1]), |
160 | '#collapsible' => TRUE, |
161 | '#collapsed' => $is_empty ? FALSE : ($index > 0), |
162 | ]; |
163 | |
164 | $form['display_links'][$index]['display_id'] = [ |
165 | '#title' => $this->t('Display'), |
166 | '#type' => 'select', |
167 | '#options' => ['' => $this->t('- Select -')] + $allowed_displays, |
168 | '#default_value' => (string) ($link['display_id'] ?? ''), |
169 | '#required' => !$is_empty, |
170 | ]; |
171 | |
172 | $form['display_links'][$index]['label'] = [ |
173 | '#title' => $this->t('Label'), |
174 | '#description' => $this->t('The text of the link.'), |
175 | '#type' => 'textfield', |
176 | '#default_value' => (string) ($link['label'] ?? ''), |
177 | '#required' => !$is_empty, |
178 | ]; |
179 | |
180 | // Only show remove button for non-empty links that are not the last |
181 | // one. |
182 | if (!$is_empty && !$is_last) { |
183 | $form['display_links'][$index]['remove'] = [ |
184 | '#type' => 'submit', |
185 | '#value' => $this->t('Remove'), |
186 | '#name' => 'remove_link_' . $index, |
187 | '#submit' => [[$this, 'removeDisplayLink']], |
188 | ]; |
189 | } |
190 | } |
191 | |
192 | } |
193 | } |
194 | |
195 | /** |
196 | * AJAX callback to remove a display link. |
197 | */ |
198 | public function removeDisplayLink(array &$form, FormStateInterface $form_state) { |
199 | $triggering_element = $form_state->getTriggeringElement(); |
200 | $index = (int) str_replace('remove_link_', '', $triggering_element['#name']); |
201 | |
202 | $display_links = $form_state->getValue('display_links', []); |
203 | unset($display_links[$index]); |
204 | // Re-index array. |
205 | $display_links = array_values($display_links); |
206 | $form_state->setValue('display_links', $display_links); |
207 | $form_state->setRebuild(); |
208 | } |
209 | |
210 | /** |
211 | * {@inheritdoc} |
212 | */ |
213 | public function validate() { |
214 | $errors = []; |
215 | |
216 | // Do not add errors for the default display if it is not displayed in the |
217 | // UI. |
218 | if ($this->displayHandler->isDefaultDisplay() && !$this->viewSettings->get('ui.show.default_display')) { |
219 | return $errors; |
220 | } |
221 | |
222 | // Validate new display links configuration. |
223 | $display_links = $this->options['display_links'] ?? []; |
224 | if (!empty($display_links)) { |
225 | foreach ($display_links as $index => $link) { |
226 | // Ensure we have valid array structure and convert values to strings. |
227 | if (!is_array($link) || empty($link['display_id']) || empty($link['label'])) { |
228 | // Skip empty links. |
229 | continue; |
230 | } |
231 | |
232 | $linked_display_id = (string) $link['display_id']; |
233 | $link_label = (string) $link['label']; |
234 | |
235 | // Check if the linked display hasn't been removed. |
236 | if (!$this->view->displayHandlers->get($linked_display_id)) { |
237 | $errors[] = $this->t('%current_display: Link @index in the %area area points to the %linked_display display which no longer exists.', [ |
238 | '%current_display' => $this->displayHandler->display['display_title'], |
239 | '@index' => $index + 1, |
240 | '%area' => $this->areaType, |
241 | '%linked_display' => $linked_display_id, |
242 | ]); |
243 | continue; |
244 | } |
245 | |
246 | // Check if the linked display is a path-based display. |
247 | if ($this->isPathBasedDisplay($linked_display_id)) { |
248 | $errors[] = $this->t('%current_display: Link @index in the %area area points to the %linked_display display which does not have a path.', [ |
249 | '%current_display' => $this->displayHandler->display['display_title'], |
250 | '@index' => $index + 1, |
251 | '%area' => $this->areaType, |
252 | '%linked_display' => $this->view->displayHandlers->get($linked_display_id)->display['display_title'], |
253 | ]); |
254 | continue; |
255 | } |
256 | |
257 | // Check if options of the linked display are equal to the options of |
258 | // the current display. We "only" show a warning here, because even |
259 | // though we recommend keeping the display options equal, we do not want |
260 | // to enforce this. |
261 | $unequal_options = [ |
262 | 'filters' => $this->t('Filter criteria'), |
263 | 'arguments' => $this->t('Contextual filters'), |
264 | ]; |
265 | foreach (array_keys($unequal_options) as $option) { |
266 | if ($this->hasEqualOptions($linked_display_id, $option)) { |
267 | unset($unequal_options[$option]); |
268 | } |
269 | } |
270 | |
271 | if ($unequal_options) { |
272 | $warning = $this->t('%current_display: Link @index in the %area area points to the %linked_display display which uses different settings than the %current_display display for: %unequal_options. To make sure users see the exact same result when clicking the link, please check that the settings are the same.', [ |
273 | '%current_display' => $this->displayHandler->display['display_title'], |
274 | '@index' => $index + 1, |
275 | '%area' => $this->areaType, |
276 | '%linked_display' => $this->view->displayHandlers->get($linked_display_id)->display['display_title'], |
277 | '%unequal_options' => implode(', ', $unequal_options), |
278 | ]); |
279 | $this->messenger()->addWarning($warning); |
280 | } |
281 | } |
282 | |
283 | // If we have display links configured, return any errors found. |
284 | if (!empty($errors)) { |
285 | return $errors; |
286 | } |
287 | } |
288 | |
289 | // If no display links are configured at all, show an error. |
290 | if (empty($display_links)) { |
291 | $errors[] = $this->t('%current_display: The link in the %area area has no configured display.', [ |
292 | '%current_display' => $this->displayHandler->display['display_title'], |
293 | '%area' => $this->areaType, |
294 | ]); |
295 | } |
296 | |
297 | return $errors; |
298 | } |
299 | |
300 | /** |
301 | * {@inheritdoc} |
302 | */ |
303 | public function render($empty = FALSE) { |
304 | if ($empty && empty($this->options['empty'])) { |
305 | return []; |
306 | } |
307 | |
308 | // Get display links configuration. |
309 | $display_links = $this->options['display_links'] ?? []; |
310 | $render_format = $this->options['render_format'] ?? 'link'; |
311 | |
312 | // Filter out empty links. |
313 | $display_links = array_filter($display_links, function ($link) { |
314 | return is_array($link) && !empty($link['display_id']) && !empty($link['label']); |
315 | }); |
316 | |
317 | if (empty($display_links)) { |
318 | return []; |
319 | } |
320 | |
321 | // Filter out path-based displays. |
322 | $valid_links = []; |
323 | foreach ($display_links as $link) { |
324 | if (!$this->isPathBasedDisplay($link['display_id'])) { |
325 | $valid_links[] = $link; |
326 | } |
327 | } |
328 | |
329 | if (empty($valid_links)) { |
330 | return []; |
331 | } |
332 | |
333 | // If only one link and format is 'link', render as single link. |
334 | if (count($valid_links) === 1 && $render_format === 'link') { |
335 | return $this->renderSingleLink($valid_links[0]); |
336 | } |
337 | |
338 | // Render based on format. |
339 | switch ($render_format) { |
340 | case 'radio': |
341 | return $this->renderRadioButtons($valid_links); |
342 | |
343 | case 'checkboxes': |
344 | return $this->renderCheckboxes($valid_links); |
345 | |
346 | case 'list': |
347 | return $this->renderUnorderedList($valid_links); |
348 | |
349 | case 'link': |
350 | default: |
351 | return $this->renderUnorderedList($valid_links); |
352 | } |
353 | } |
354 | |
355 | /** |
356 | * Renders a single display link. |
357 | * |
358 | * @param array $link |
359 | * The link configuration. |
360 | * |
361 | * @return array |
362 | * The render array for a single link. |
363 | */ |
364 | protected function renderSingleLink(array $link): array { |
365 | $label = (string) $link['label']; |
366 | $display_id = (string) $link['display_id']; |
367 | $bidirectional_label = (string) ($this->options['bidirectional_label'] ?? ''); |
368 | |
369 | // Get query parameters from the exposed input and pager. |
370 | $query = $this->view->getExposedInput(); |
371 | if ($current_page = $this->view->getCurrentPage()) { |
372 | $query['page'] = $current_page; |
373 | } |
374 | |
375 | // @todo Remove this parsing once these are removed from the request in |
376 | // https://www.drupal.org/node/2504709. |
377 | foreach ([ |
378 | 'view_name', |
379 | 'view_display_id', |
380 | 'view_args', |
381 | 'view_path', |
382 | 'view_dom_id', |
383 | 'pager_element', |
384 | 'view_base_path', |
385 | AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER, |
386 | FormBuilderInterface::AJAX_FORM_REQUEST, |
387 | MainContentViewSubscriber::WRAPPER_FORMAT, |
388 | ] as $key) { |
389 | unset($query[$key]); |
390 | } |
391 | |
392 | // Check for bidirectional linking information in drupal_static. |
393 | $bidirectional_info = drupal_static('visitors_bidirectional_linking', []); |
394 | if (isset($bidirectional_info[$label])) { |
395 | $override_info = $bidirectional_info[$label]; |
396 | $storage_id = $override_info['view_id']; |
397 | $display_id = $override_info['display_id']; |
398 | } |
399 | else { |
400 | $storage_id = $this->view->storage->id(); |
401 | } |
402 | |
403 | // Set default classes. |
404 | $classes = [ |
405 | 'views-display-link', |
406 | 'views-display-link-' . $display_id, |
407 | ]; |
408 | if ($display_id === $this->view->current_display) { |
409 | $classes[] = 'is-active'; |
410 | } |
411 | $classes[] = 'use-ajax'; |
412 | |
413 | $path = 'internal:/admin/visitors/_report/' . $storage_id . '/' . $display_id; |
414 | $query_class = '.view-id-' . $storage_id . '.view-display-id-' . $this->view->current_display; |
415 | |
416 | // Add bidirectional linking information to the query parameters. |
417 | $query_params = ['class' => $query_class]; |
418 | if (!empty($bidirectional_label)) { |
419 | $query_params['bidirectional_label'] = $bidirectional_label; |
420 | } |
421 | |
422 | return [ |
423 | '#type' => 'link', |
424 | '#title' => $label, |
425 | '#url' => Url::fromUri($path, [ |
426 | 'query' => $query_params, |
427 | ]), |
428 | '#options' => [ |
429 | 'attributes' => ['class' => $classes], |
430 | ], |
431 | ]; |
432 | } |
433 | |
434 | /** |
435 | * Renders display links as radio buttons. |
436 | * |
437 | * @param array $links |
438 | * Array of link configurations. |
439 | * |
440 | * @return array |
441 | * The render array for radio buttons. |
442 | */ |
443 | protected function renderRadioButtons(array $links): array { |
444 | $options = []; |
445 | $default_value = NULL; |
446 | |
447 | foreach ($links as $link) { |
448 | $display_id = (string) $link['display_id']; |
449 | $label = (string) $link['label']; |
450 | |
451 | $options[$display_id] = $label; |
452 | |
453 | if ($display_id === $this->view->current_display) { |
454 | $default_value = $display_id; |
455 | } |
456 | } |
457 | |
458 | return [ |
459 | '#type' => 'radios', |
460 | '#title' => $this->t('Display Options'), |
461 | '#options' => $options, |
462 | '#default_value' => $default_value, |
463 | '#attributes' => [ |
464 | 'class' => ['visitors-display-radios'], |
465 | 'data-ajax' => 'true', |
466 | ], |
467 | '#ajax' => [ |
468 | 'callback' => [$this, 'ajaxDisplayChange'], |
469 | 'wrapper' => 'visitors-display-content', |
470 | ], |
471 | ]; |
472 | } |
473 | |
474 | /** |
475 | * Renders display links as checkboxes. |
476 | * |
477 | * @param array $links |
478 | * Array of link configurations. |
479 | * |
480 | * @return array |
481 | * The render array for checkboxes. |
482 | */ |
483 | protected function renderCheckboxes(array $links): array { |
484 | $options = []; |
485 | $default_value = []; |
486 | |
487 | foreach ($links as $link) { |
488 | $display_id = (string) $link['display_id']; |
489 | $label = (string) $link['label']; |
490 | |
491 | $options[$display_id] = $label; |
492 | |
493 | if ($display_id === $this->view->current_display) { |
494 | $default_value[] = $display_id; |
495 | } |
496 | } |
497 | |
498 | return [ |
499 | '#type' => 'checkboxes', |
500 | '#title' => $this->t('Display Options'), |
501 | '#options' => $options, |
502 | '#default_value' => $default_value, |
503 | '#attributes' => [ |
504 | 'class' => ['visitors-display-checkboxes'], |
505 | 'data-ajax' => 'true', |
506 | ], |
507 | '#ajax' => [ |
508 | 'callback' => [$this, 'ajaxDisplayChange'], |
509 | 'wrapper' => 'visitors-display-content', |
510 | ], |
511 | ]; |
512 | } |
513 | |
514 | /** |
515 | * Renders display links as an unordered list. |
516 | * |
517 | * @param array $links |
518 | * Array of link configurations. |
519 | * |
520 | * @return array |
521 | * The render array for an unordered list. |
522 | */ |
523 | protected function renderUnorderedList(array $links): array { |
524 | $items = []; |
525 | |
526 | foreach ($links as $link) { |
527 | $items[] = $this->renderSingleLink($link); |
528 | } |
529 | |
530 | return [ |
531 | '#theme' => 'item_list', |
532 | '#items' => $items, |
533 | '#attributes' => [ |
534 | 'class' => ['visitors-display-links'], |
535 | ], |
536 | ]; |
537 | } |
538 | |
539 | /** |
540 | * AJAX callback for display change. |
541 | * |
542 | * @param array $form |
543 | * The form array. |
544 | * @param \Drupal\Core\Form\FormStateInterface $form_state |
545 | * The form state. |
546 | * |
547 | * @return array |
548 | * The AJAX response. |
549 | */ |
550 | public function ajaxDisplayChange(array &$form, FormStateInterface $form_state) { |
551 | // This would need to be implemented based on specific requirements |
552 | // for handling display changes via AJAX. |
553 | return []; |
554 | } |
555 | |
556 | } |