Unverified Commit 9c8eb293 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #343535 by merlinofchaos, nod_, matthiasm11, prudloff, elaman,...

Issue #343535 by merlinofchaos, nod_, matthiasm11, prudloff, elaman, smustgrave, claudiu.cristea, pooja saraah, shabana.navas, gaëlg, alexpott: Enable bookmarking of AJAX views
parent d4b7f49d
Loading
Loading
Loading
Loading
Loading
+42 −11
Original line number Diff line number Diff line
@@ -63,6 +63,12 @@
  Drupal.views.ajaxView = function (settings) {
    const selector = `.js-view-dom-id-${settings.view_dom_id}`;
    this.$view = $(selector);
    this.$exposed_form = $(
      `form#views-exposed-form-${settings.view_name.replace(
        /_/g,
        '-',
      )}-${settings.view_display_id.replace(/_/g, '-')}`,
    );

    // Retrieve the path to use for views' ajax.
    let ajaxPath = drupalSettings.views.ajax_path;
@@ -77,12 +83,26 @@
    let queryString = window.location.search || '';
    if (queryString !== '') {
      // Remove the question mark and Drupal path component if any.
      queryString = queryString
        .slice(1)
        .replace(/q=[^&]+&?|page=[^&]+&?|&?render=[^&]+/, '');
      if (queryString !== '') {
      queryString = queryString.slice(1);

      // Remove current exposed filters.
      const params = decodeURI(queryString)
        .split('&')
        .filter((param) => {
          const [name, value] = param.split('=');
          return (
            this.$exposed_form.find(`input[name="${name}"]`).length === 0 &&
            /*
            Submitting filters should reset paging and sorting
            because that is what happens without AJAX.
             */
            !['page', 'reset', 'sort', 'order', 'q', 'render'].includes(name)
          );
        });
      queryString = encodeURI(params.join('&'));
      // If there is a '?' in ajaxPath, clean URL are on and & should be
      // used to add parameters.
      if (queryString !== '') {
        queryString = (ajaxPath.includes('?') ? '&' : '?') + queryString;
      }
    }
@@ -100,12 +120,6 @@
    this.settings = settings;

    // Add the ajax to exposed forms.
    this.$exposed_form = $(
      `form#views-exposed-form-${settings.view_name.replace(
        /_/g,
        '-',
      )}-${settings.view_display_id.replace(/_/g, '-')}`,
    );
    once('exposed-form', this.$exposed_form).forEach(
      this.attachExposedFormAjax.bind(this),
    );
@@ -207,4 +221,21 @@
    });
    this.pagerAjax = Drupal.ajax(selfSettings);
  };

  /**
   * Sets the browser URL ajax command.
   *
   * @param {Drupal.Ajax} [ajax]
   *   A {@link Drupal.ajax} object.
   * @param {object} response
   *   Ajax response.
   * @param {string} response.url
   *   URL to be set.
   */
  Drupal.AjaxCommands.prototype.setBrowserUrl = (ajax, response) => {
    // Do not change browser URL if we are in a dialog wrapper.
    if (ajax.element && !ajax.element.closest('.ui-dialog-content')) {
      window.history.replaceState(null, '', response.url);
    }
  };
})(jQuery, Drupal, drupalSettings);
+33 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\views\Ajax;

use Drupal\Core\Ajax\CommandInterface;

/**
 * AJAX command that sets the browser URL without refreshing the page.
 *
 * This command is implemented in Drupal.AjaxCommands.prototype.setBrowserUrl.
 */
class SetBrowserUrl implements CommandInterface {

  /**
   * Constructs a new command instance.
   *
   * @param string $url
   *   The URL to be set in the browser.
   */
  public function __construct(protected string $url) {
  }

  /**
   * {@inheritdoc}
   */
  public function render(): array {
    return [
      'command' => 'setBrowserUrl',
      'url' => $this->url,
    ];
  }

}
+15 −56
Original line number Diff line number Diff line
@@ -11,9 +11,11 @@
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Path\CurrentPathStack;
use Drupal\Core\Path\PathValidatorInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RedirectDestinationInterface;
use Drupal\Core\Ajax\ScrollTopCommand;
use Drupal\views\Ajax\SetBrowserUrl;
use Drupal\views\Ajax\ViewAjaxResponse;
use Drupal\views\ViewExecutableFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -43,61 +45,14 @@ class ViewAjaxController implements ContainerInjectionInterface {
    MainContentViewSubscriber::WRAPPER_FORMAT,
  ];

  /**
   * The entity storage for views.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $storage;

  /**
   * The factory to load a view executable with.
   *
   * @var \Drupal\views\ViewExecutableFactory
   */
  protected $executableFactory;

  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\RendererInterface
   */
  protected $renderer;

  /**
   * The current path.
   *
   * @var \Drupal\Core\Path\CurrentPathStack
   */
  protected $currentPath;

  /**
   * The redirect destination.
   *
   * @var \Drupal\Core\Routing\RedirectDestinationInterface
   */
  protected $redirectDestination;

  /**
   * Constructs a ViewAjaxController object.
   *
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The entity storage for views.
   * @param \Drupal\views\ViewExecutableFactory $executable_factory
   *   The factory to load a view executable with.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The renderer.
   * @param \Drupal\Core\Path\CurrentPathStack $current_path
   *   The current path.
   * @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination
   *   The redirect destination.
   */
  public function __construct(EntityStorageInterface $storage, ViewExecutableFactory $executable_factory, RendererInterface $renderer, CurrentPathStack $current_path, RedirectDestinationInterface $redirect_destination) {
    $this->storage = $storage;
    $this->executableFactory = $executable_factory;
    $this->renderer = $renderer;
    $this->currentPath = $current_path;
    $this->redirectDestination = $redirect_destination;
  public function __construct(
    protected EntityStorageInterface $storage,
    protected ViewExecutableFactory $executableFactory,
    protected RendererInterface $renderer,
    protected CurrentPathStack $currentPath,
    protected RedirectDestinationInterface $redirectDestination,
    protected PathValidatorInterface $pathValidator,
  ) {
  }

  /**
@@ -109,7 +64,8 @@ public static function create(ContainerInterface $container) {
      $container->get('views.executable'),
      $container->get('renderer'),
      $container->get('path.current'),
      $container->get('redirect.destination')
      $container->get('redirect.destination'),
      $container->get('path.validator')
    );
  }

@@ -139,6 +95,7 @@ public function ajaxView(Request $request) {
      }, $args);

      $path = $request->get('view_path');
      $target_url = $this->pathValidator->getUrlIfValid($path ?? '');
      $dom_id = $request->get('view_dom_id');
      $dom_id = isset($dom_id) ? preg_replace('/[^a-zA-Z0-9_-]+/', '-', $dom_id) : NULL;
      $pager_element = $request->get('pager_element');
@@ -187,6 +144,7 @@ public function ajaxView(Request $request) {
        $query = UrlHelper::buildQuery($used_query_parameters);
        if ($query != '') {
          $origin_destination .= '?' . $query;
          $target_url->setOption('query', $used_query_parameters);
        }
        $this->redirectDestination->set($origin_destination);

@@ -210,6 +168,7 @@ public function ajaxView(Request $request) {
        }
        $preview = $view->preview($display_id, $args);
        $request->attributes->remove('ajax_page_state');
        $response->addCommand(new SetBrowserUrl($target_url->toString()));
        $response->addCommand(new ReplaceCommand(".js-view-dom-id-$dom_id", $preview));
        $response->addCommand(new PrependCommand(".js-view-dom-id-$dom_id", ['#type' => 'status_messages']));
        $request->query->set('ajax_page_state', $existing_page_state);
+5 −0
Original line number Diff line number Diff line
@@ -97,6 +97,7 @@ public function testExposedFiltering(): void {
    $html = $session->getPage()->getHtml();
    $this->assertStringContainsString('Page One', $html);
    $this->assertStringContainsString('Page Two', $html);
    $this->assertSession()->addressEquals('admin/content');

    // Search for "Page One".
    $this->submitForm(['title' => 'Page One'], 'Filter');
@@ -106,6 +107,7 @@ public function testExposedFiltering(): void {
    $html = $session->getPage()->getHtml();
    $this->assertStringContainsString('Page One', $html);
    $this->assertStringNotContainsString('Page Two', $html);
    $this->assertSession()->addressEquals('admin/content?title=Page%20One&type=All&status=All');

    // Search for "Page Two".
    $this->submitForm(['title' => 'Page Two'], 'Filter');
@@ -115,6 +117,7 @@ public function testExposedFiltering(): void {
    $html = $session->getPage()->getHtml();
    $this->assertStringContainsString('Page Two', $html);
    $this->assertStringNotContainsString('Page One', $html);
    $this->assertSession()->addressEquals('admin/content?type=All&status=All&title=Page%20Two');

    // Submit bulk actions form to ensure that the previous AJAX submit does not
    // break it.
@@ -125,6 +128,7 @@ public function testExposedFiltering(): void {

    // Verify that the action was performed.
    $this->assertSession()->pageTextContains('Make content sticky was applied to 1 item.');
    $this->assertSession()->addressEquals('admin/content?type=All&status=All&title=Page%20Two');

    // Reset the form.
    $this->submitForm([], 'Reset');
@@ -132,6 +136,7 @@ public function testExposedFiltering(): void {
    $this->assertSession()->pageTextContains('Page One');
    $this->assertSession()->pageTextContains('Page Two');
    $this->assertFalse($session->getPage()->hasButton('Reset'));
    $this->assertSession()->addressEquals('admin/content');
  }

  /**
+3 −3
Original line number Diff line number Diff line
@@ -115,7 +115,7 @@ public function testBasicPagination(): void {
    $this->assertNoDuplicateAssetsOnPage();

    // Test that no unwanted parameters are added to the URL.
    $this->assertEquals('?status=All&type=All&title=&items_per_page=5&order=changed&sort=asc&page=2', $link->getAttribute('href'));
    $this->assertEquals('?status=All&type=All&items_per_page=5&title=&order=changed&sort=asc&page=2', $link->getAttribute('href'));

    $this->clickLink('Go to page 3');
    $session_assert->assertWaitOnAjaxRequest();
@@ -199,7 +199,7 @@ public function testDefaultFilterPagination(): void {
    $this->assertNoDuplicateAssetsOnPage();

    // Test that no unwanted parameters are added to the URL.
    $this->assertEquals('?status=All&type=All&title=default_value&items_per_page=5&order=changed&sort=asc&page=0', $link->getAttribute('href'));
    $this->assertEquals('?status=All&type=All&items_per_page=5&title=default_value&order=changed&sort=asc&page=0', $link->getAttribute('href'));

    // Set the title filter to empty string using the exposed pager.
    $page->fillField('title', '');
@@ -219,7 +219,7 @@ public function testDefaultFilterPagination(): void {
    $this->assertNoDuplicateAssetsOnPage();

    // Test that no unwanted parameters are added to the URL.
    $this->assertEquals('?status=All&type=All&title=&items_per_page=5&page=0', $link->getAttribute('href'));
    $this->assertEquals('?status=All&type=All&items_per_page=5&title=&page=0', $link->getAttribute('href'));

    // Navigate back to the first page.
    $this->clickLink('Go to first page');
Loading