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