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 | } |