Loading core/modules/views/js/ajax_view.js +42 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; } } Loading @@ -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), ); Loading Loading @@ -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); core/modules/views/src/Ajax/SetBrowserUrl.php 0 → 100644 +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, ]; } } core/modules/views/src/Controller/ViewAjaxController.php +15 −56 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, ) { } /** Loading @@ -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') ); } Loading Loading @@ -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'); Loading Loading @@ -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); Loading @@ -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); Loading core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php +5 −0 Original line number Diff line number Diff line Loading @@ -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'); Loading @@ -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'); Loading @@ -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. Loading @@ -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'); Loading @@ -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'); } /** Loading core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php +3 −3 Original line number Diff line number Diff line Loading @@ -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(); Loading Loading @@ -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', ''); Loading @@ -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 Loading
core/modules/views/js/ajax_view.js +42 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; } } Loading @@ -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), ); Loading Loading @@ -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);
core/modules/views/src/Ajax/SetBrowserUrl.php 0 → 100644 +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, ]; } }
core/modules/views/src/Controller/ViewAjaxController.php +15 −56 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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, ) { } /** Loading @@ -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') ); } Loading Loading @@ -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'); Loading Loading @@ -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); Loading @@ -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); Loading
core/modules/views/tests/src/FunctionalJavascript/ExposedFilterAJAXTest.php +5 −0 Original line number Diff line number Diff line Loading @@ -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'); Loading @@ -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'); Loading @@ -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. Loading @@ -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'); Loading @@ -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'); } /** Loading
core/modules/views/tests/src/FunctionalJavascript/PaginationAJAXTest.php +3 −3 Original line number Diff line number Diff line Loading @@ -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(); Loading Loading @@ -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', ''); Loading @@ -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