From 14fb6907ad432672eed4348b27341eecda21f359 Mon Sep 17 00:00:00 2001 From: borisson_ <borisson_@2393360.no-reply.drupal.org> Date: Thu, 5 Jul 2018 17:51:26 +0100 Subject: [PATCH] Issue #2826449 by piyuesh23, borisson_, RumyanaRuseva, gaydabura, andypost, niknak, tomasbarej, MDJ Webdiensten, recrit, vasike, acbramley, cato, flocondetoile, YurkinPark, StryKaizer, nikunjkotecha, diegodalr3, finnsky, acrollet, meanderix, uzlov, denis_kv, abhishek-anand, sylus, Nick_vh: Ajax Facets blocks for views --- facets.install | 18 ++ facets.libraries.yml | 9 + facets.routing.yml | 7 + js/checkbox-widget.js | 4 +- js/dropdown-widget.js | 6 +- js/facets-views-ajax.js | 203 ++++++++++++++++++ .../src/Plugin/Block/FacetsSummaryBlock.php | 13 ++ src/Controller/FacetBlockAjaxController.php | 179 +++++++++++++++ src/FacetManager/DefaultFacetManager.php | 14 +- src/FacetSource/FacetSourcePluginBase.php | 7 + .../FacetSourcePluginInterface.php | 10 + src/Plugin/Block/FacetBlock.php | 32 ++- .../facets/facet_source/SearchApiDisplay.php | 28 +++ src/Widget/WidgetPluginBase.php | 16 ++ .../Functional/ProcessorIntegrationTest.php | 2 +- tests/src/Functional/TestHelperTrait.php | 9 +- .../FunctionalJavascript/AjaxBehaviorTest.php | 69 ++++++ tests/src/FunctionalJavascript/JsBase.php | 168 +++++++++++++++ .../src/FunctionalJavascript/WidgetJSTest.php | 121 +---------- 19 files changed, 785 insertions(+), 130 deletions(-) create mode 100644 js/facets-views-ajax.js create mode 100644 src/Controller/FacetBlockAjaxController.php create mode 100644 tests/src/FunctionalJavascript/AjaxBehaviorTest.php create mode 100644 tests/src/FunctionalJavascript/JsBase.php diff --git a/facets.install b/facets.install index 82a9f6b2..7678e817 100644 --- a/facets.install +++ b/facets.install @@ -7,6 +7,7 @@ use Drupal\facets\Entity\Facet; use Drupal\facets\Entity\FacetSource; +use Drupal\block\Entity\Block; /** * Convert facets on Search Api facet sources to use the display plugin. @@ -149,3 +150,20 @@ function facets_update_8005() { } } } + +/** + * Update facet blocks configuration with a block id used for AJAX support. + */ +function facets_update_8006() { + $query = \Drupal::entityQuery('block') + ->condition('plugin', 'facet_block', 'STARTS_WITH') + ->execute(); + + foreach ($query as $block_id) { + $block = Block::load($block_id); + $configuration = $block->get('settings'); + $configuration['block_id'] = $block_id; + $block->set('settings', $configuration); + $block->save(); + } +} diff --git a/facets.libraries.yml b/facets.libraries.yml index 2ebbf328..a77986de 100644 --- a/facets.libraries.yml +++ b/facets.libraries.yml @@ -53,3 +53,12 @@ soft-limit: - core/jquery.once - core/drupal - core/drupalSettings +drupal.facets.views-ajax: + js: + js/facets-views-ajax.js: {} + dependencies: + - core/jquery + - core/jquery.once + - core/drupal + - core/drupalSettings + - core/drupal.ajax diff --git a/facets.routing.yml b/facets.routing.yml index 1e897e76..9f269117 100644 --- a/facets.routing.yml +++ b/facets.routing.yml @@ -48,3 +48,10 @@ entity.facets_facet_source.edit_form: _title: 'Edit facet source configuration' requirements: _entity_create_access: 'facets_facet' + +facets.block.ajax: + path: '/facets-block-ajax' + defaults: + _controller: '\Drupal\facets\Controller\FacetBlockAjaxController::ajaxFacetBlockView' + requirements: + _access: 'TRUE' diff --git a/js/checkbox-widget.js b/js/checkbox-widget.js index 335d9249..b6f00f72 100644 --- a/js/checkbox-widget.js +++ b/js/checkbox-widget.js @@ -43,7 +43,7 @@ checkbox.on('change.facets', function (e) { Drupal.facets.disableFacet($link.parents('.js-facets-checkbox-links')); - window.location.href = $(this).data('facetsredir'); + $(this).siblings('a')[0].click(); }); if (active) { @@ -51,7 +51,7 @@ label.find('.js-facet-deactivate').remove(); } - $link.before(checkbox).before(label).remove(); + $link.before(checkbox).before(label).hide(); }; diff --git a/js/dropdown-widget.js b/js/dropdown-widget.js index e87b929e..1e790c5f 100644 --- a/js/dropdown-widget.js +++ b/js/dropdown-widget.js @@ -38,6 +38,7 @@ $dropdown.removeClass('js-facets-dropdown-links'); $dropdown.addClass('facets-dropdown'); + $dropdown.addClass('js-facets-dropdown'); var id = $(this).data('drupal-facet-id'); var default_option_label = settings.facets.dropdown_widget[id]['facet-default-option-label']; @@ -68,7 +69,8 @@ // Go to the selected option when it's clicked. $dropdown.on('change.facets', function () { - window.location.href = $(this).val(); + var a = $($ul).find("[data-drupal-facet-item-id='" + $(this).find(':selected').data('drupalFacetItemId') + "']"); + $(a)[0].click(); }); // Append empty text option. @@ -77,7 +79,7 @@ } // Replace links with dropdown. - $ul.after($dropdown).remove(); + $ul.after($dropdown).hide(); Drupal.attachBehaviors($dropdown.parent()[0], Drupal.settings); }); }; diff --git a/js/facets-views-ajax.js b/js/facets-views-ajax.js new file mode 100644 index 00000000..3f2fac5d --- /dev/null +++ b/js/facets-views-ajax.js @@ -0,0 +1,203 @@ +/** + * @file + * Facets views AJAX handling. + */ + + +(function ($, Drupal) { + 'use strict'; + + /** + * Keep the original beforeSend method to use it later. + */ + var beforeSend = Drupal.Ajax.prototype.beforeSend; + + /** + * Trigger views AJAX refresh on click. + */ + Drupal.behaviors.facetsViewsAjax = { + attach: function (context, settings) { + + // Loop through all facets. + $.each(settings.facets_views_ajax, function (facetId, facetSettings) { + // Get the View for the current facet. + var view, current_dom_id, view_path; + if (settings.views && settings.views.ajaxViews) { + $.each(settings.views.ajaxViews, function (domId, viewSettings) { + // Check if we have facet for this view. + if (facetSettings.view_id == viewSettings.view_name && facetSettings.current_display_id == viewSettings.view_display_id) { + view = $('.js-view-dom-id-' + viewSettings.view_dom_id); + current_dom_id = viewSettings.view_dom_id; + view_path = facetSettings.ajax_path; + } + }); + } + + if (!view || view.length != 1) { + return; + } + + // Update view on summary block click. + if (updateFacetsSummaryBlock() && (facetId === 'facets_summary_ajax')) { + $('[data-drupal-facets-summary-id=' + facetSettings.facets_summary_id + ']').children('ul').children('li').once().click(function (e) { + e.preventDefault(); + var facetLink = $(this).find('a'); + updateFacetsView(facetLink.attr('href'), current_dom_id, view_path); + }); + } + // Update view on facet item click. + else { + $('[data-drupal-facet-id=' + facetId + ']').find('.facet-item').once().each(function (index, facet_item) { + $(facet_item).children('a').once().click(function (e) { + e.preventDefault(); + updateFacetsView($(this).attr('href'), current_dom_id, view_path); + }); + }); + + $('[data-drupal-facet-id=' + facetId + ']').each(function (index, facet_item) { + if ($(facet_item).hasClass('js-facets-dropdown')) { + $(facet_item).unbind('change.facets'); + $(facet_item).on('change.facets', function () { + updateFacetsView($(this).val(), current_dom_id, view_path); + }); + } + }); + + } + }); + } + }; + + // Helper function to update views output & Ajax facets. + var updateFacetsView = function (href, current_dom_id, view_path) { + // Refresh view. + var views_parameters = Drupal.Views.parseQueryString(href); + var views_arguments = Drupal.Views.parseViewArgs(href, 'search'); + var views_settings = $.extend( + {}, + Drupal.views.instances['views_dom_id:' + current_dom_id].settings, + views_arguments, + views_parameters + ); + + // Update View. + var views_ajax_settings = Drupal.views.instances['views_dom_id:' + current_dom_id].element_settings; + views_ajax_settings.submit = views_settings; + views_ajax_settings.url = view_path + '?q=' + href; + + Drupal.ajax(views_ajax_settings).execute(); + + // Update url. + window.historyInitiated = true; + window.history.pushState(null, document.title, href); + + // ToDo: Update views+facets with ajax on history back. + // For now we will reload the full page. + window.addEventListener("popstate", function (e) { + if (window.historyInitiated) { + window.location.reload(); + } + }); + + // Refresh facets blocks. + updateFacetsBlocks(href); + } + + // Helper function, updates facet blocks. + var updateFacetsBlocks = function (href) { + var settings = drupalSettings; + var facets_blocks = facetsBlocks(); + + // Update facet blocks. + let facet_settings = { + url: Drupal.url('facets-block-ajax'), + submit: { + facet_link: href, + facets_blocks: facets_blocks + } + }; + + // Update facets summary block. + if (updateFacetsSummaryBlock()) { + var facet_summary_wrapper_id = $('[data-drupal-facets-summary-id=' + settings.facets_views_ajax.facets_summary_ajax.facets_summary_id + ']').attr('id'); + var facet_summary_block_id = ''; + if (facet_summary_wrapper_id.indexOf('--') !== -1) { + facet_summary_block_id = facet_summary_wrapper_id.substring(0, facet_summary_wrapper_id.indexOf('--')).replace('block-', ''); + } + else { + facet_summary_block_id = facet_summary_wrapper_id.replace('block-', ''); + } + facet_settings.submit.update_summary_block = true; + facet_settings.submit.facet_summary_block_id = facet_summary_block_id; + facet_settings.submit.facet_summary_wrapper_id = settings.facets_views_ajax.facets_summary_ajax.facets_summary_id; + } + + Drupal.ajax(facet_settings).execute(); + }; + + // Helper function to determine if we should update the summary block. + // Returns true or false. + var updateFacetsSummaryBlock = function () { + var settings = drupalSettings; + var update_summary = false; + + if (settings.facets_views_ajax.facets_summary_ajax) { + update_summary = true; + } + + return update_summary; + }; + + // Helper function, return facet blocks. + var facetsBlocks = function () { + // Get all ajax facets blocks from the current page. + var facets_blocks = {}; + + $('.block-facets-ajax').each(function (index) { + var block_id_start = 'js-facet-block-id-'; + var block_id = $.map($(this).attr('class').split(' '), function (v, i) { + if (v.indexOf(block_id_start) > -1) { + return v.slice(block_id_start.length, v.length); + } + }).join(); + var block_selector = '#' + $(this).attr('id'); + facets_blocks[block_id] = block_selector; + }); + + return facets_blocks; + }; + + /** + * Overrides beforeSend to trigger facetblocks update on exposed filter change. + * + * @param {XMLHttpRequest} xmlhttprequest + * Native Ajax object. + * @param {object} options + * jQuery.ajax options. + */ + Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) { + + // Update facet blocks as well. + // Get view from options. + if (typeof options.extraData !== 'undefined' && typeof options.extraData.view_name !== 'undefined') { + var href = window.location.href; + var settings = drupalSettings; + + // TODO: Maybe we should limit facet block reloads by view? + var reload = false; + $.each(settings.facets_views_ajax, function (facetId, facetSettings) { + if (facetSettings.view_id == options.extraData.view_name && facetSettings.current_display_id == options.extraData.view_display_id) { + reload = true; + } + }); + + if (reload) { + updateFacetsBlocks(href); + } + } + + // Call the original Drupal method with the right context. + beforeSend.apply(this, arguments); + } + +})(jQuery, Drupal); diff --git a/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php b/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php index 93ccd99c..54c476b3 100644 --- a/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php +++ b/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php @@ -5,6 +5,7 @@ namespace Drupal\facets_summary\Plugin\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Cache\UncacheableDependencyTrait; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Url; use Drupal\facets_summary\Entity\FacetsSummary; use Drupal\facets_summary\FacetsSummaryBlockInterface; use Drupal\facets_summary\FacetsSummaryManager\DefaultFacetsSummaryManager; @@ -96,6 +97,18 @@ class FacetsSummaryBlock extends BlockBase implements FacetsSummaryBlockInterfac ]; } + /** @var \Drupal\views\ViewExecutable $view */ + $view = $facets_summary->getFacetSource()->getViewsDisplay(); + + $build['#attached']['drupalSettings']['facets_views_ajax'] = [ + 'facets_summary_ajax' => [ + 'facets_summary_id' => $facets_summary->id(), + 'view_id' => $view->id(), + 'current_display_id' => $view->current_display, + 'ajax_path' => Url::fromRoute('views.ajax')->toString(), + ], + ]; + return $build; } diff --git a/src/Controller/FacetBlockAjaxController.php b/src/Controller/FacetBlockAjaxController.php new file mode 100644 index 00000000..ce43a99b --- /dev/null +++ b/src/Controller/FacetBlockAjaxController.php @@ -0,0 +1,179 @@ +<?php + +namespace Drupal\facets\Controller; + +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\InvokeCommand; +use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Path\CurrentPathStack; +use Drupal\Core\PathProcessor\PathProcessorManager; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Routing\CurrentRouteMatch; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\RouterInterface; + +/** + * Defines a controller to load a facet via AJAX. + */ +class FacetBlockAjaxController extends ControllerBase { + + /** + * The entity storage for block. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $storage; + + /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * The current path. + * + * @var \Drupal\Core\Path\CurrentPathStack + */ + protected $currentPath; + + /** + * The dynamic router service. + * + * @var \Symfony\Component\Routing\Matcher\RequestMatcherInterface + */ + protected $router; + + /** + * The path processor service. + * + * @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface + */ + protected $pathProcessor; + + /** + * The current route match service. + * + * @var \Drupal\Core\Routing\CurrentRouteMatch + */ + protected $currentRouteMatch; + + /** + * Constructs a FacetBlockAjaxController object. + * + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + * @param \Drupal\Core\Path\CurrentPathStack $currentPath + * The current path service. + * @param \Symfony\Component\Routing\RouterInterface $router + * The router service. + * @param \Drupal\Core\PathProcessor\PathProcessorManager $pathProcessor + * The path processor manager. + * @param \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatch + * The current route match service. + */ + public function __construct(RendererInterface $renderer, CurrentPathStack $currentPath, RouterInterface $router, PathProcessorManager $pathProcessor, CurrentRouteMatch $currentRouteMatch) { + $this->storage = $this->entityTypeManager()->getStorage('block'); + $this->renderer = $renderer; + $this->currentPath = $currentPath; + $this->router = $router; + $this->pathProcessor = $pathProcessor; + $this->currentRouteMatch = $currentRouteMatch; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('renderer'), + $container->get('path.current'), + $container->get('router'), + $container->get('path_processor_manager'), + $container->get('current_route_match') + ); + } + + /** + * Loads and renders the facet blocks via AJAX. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * The ajax response. + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * Thrown when the view was not found. + */ + public function ajaxFacetBlockView(Request $request) { + $response = new AjaxResponse(); + + // Rebuild the request and the current path, needed for facets. + $path = $request->request->get('facet_link'); + $facets_blocks = $request->request->get('facets_blocks'); + + // Make sure we are not updating blocks multiple times. + $facets_blocks = array_unique($facets_blocks); + + if (empty($path) || empty($facets_blocks)) { + throw new NotFoundHttpException('No facet link or facet blocks found.'); + } + + $this->currentRouteMatch->resetRouteMatch(); + $new_request = Request::create($path); + $request_stack = new RequestStack(); + $processed = $this->pathProcessor->processInbound($path, $new_request); + + $this->currentPath->setPath($processed, $new_request); + $request->attributes->add($this->router->matchRequest($new_request)); + $request_stack->push($new_request); + + $container = \Drupal::getContainer(); + $container->set('request_stack', $request_stack); + $active_facet = $request->request->get('active_facet'); + + // Build the facets blocks found for the current request and update. + foreach ($facets_blocks as $block_id => $block_selector) { + $block_entity = $this->storage->load($block_id); + + if ($block_entity) { + // Render a block, then add it to the response as a replace command. + $block_view = $this->entityTypeManager + ->getViewBuilder('block') + ->view($block_entity); + + $block_view = (string) $this->renderer->renderPlain($block_view); + $response->addCommand(new ReplaceCommand($block_selector, $block_view)); + } + } + + $response->addCommand(new InvokeCommand('[data-block-plugin-id="' . $active_facet . '"]', 'addClass', ['facet-active'])); + + // Update filter summary block. + $update_summary_block = $request->request->get('update_summary_block'); + if ($update_summary_block) { + $facet_summary_block_id = $request->request->get('facet_summary_block_id'); + $facet_summary_wrapper_id = $request->request->get('facet_summary_wrapper_id'); + $facet_summary_block_id = str_replace('-', '_', $facet_summary_block_id); + + if ($facet_summary_block_id) { + $block_entity = $this->storage->load($facet_summary_block_id); + $block_view = $this->entityTypeManager + ->getViewBuilder('block') + ->view($block_entity); + $block_view = (string) $this->renderer->renderPlain($block_view); + + $response->addCommand(new ReplaceCommand('[data-drupal-facets-summary-id=' . $facet_summary_wrapper_id . ']', $block_view)); + } + } + + return $response; + } + +} diff --git a/src/FacetManager/DefaultFacetManager.php b/src/FacetManager/DefaultFacetManager.php index 7e33a30f..4ccacce0 100644 --- a/src/FacetManager/DefaultFacetManager.php +++ b/src/FacetManager/DefaultFacetManager.php @@ -334,7 +334,19 @@ class DefaultFacetManager { ]; } else { - return []; + // If the facet has no results, but it is being rendered trough ajax we + // should render a container (that is empty). This is because the + // javascript needs to be able to find a div to replace with the new + // content. + return [ + [ + '#type' => 'container', + '#attributes' => [ + 'data-drupal-facet-id' => $facet->id(), + 'class' => 'facet-empty', + ], + ], + ]; } } diff --git a/src/FacetSource/FacetSourcePluginBase.php b/src/FacetSource/FacetSourcePluginBase.php index 14317fbf..9a470f8f 100644 --- a/src/FacetSource/FacetSourcePluginBase.php +++ b/src/FacetSource/FacetSourcePluginBase.php @@ -116,6 +116,13 @@ abstract class FacetSourcePluginBase extends PluginBase implements FacetSourcePl return $this->keys; } + /** + * {@inheritdoc} + */ + public function buildFacet() { + return []; + } + /** * {@inheritdoc} */ diff --git a/src/FacetSource/FacetSourcePluginInterface.php b/src/FacetSource/FacetSourcePluginInterface.php index d88d3e5f..1b971012 100644 --- a/src/FacetSource/FacetSourcePluginInterface.php +++ b/src/FacetSource/FacetSourcePluginInterface.php @@ -105,4 +105,14 @@ interface FacetSourcePluginInterface extends PluginFormInterface, DependentPlugi */ public function getDataDefinition($field_name); + /** + * Builds and returns an extra renderable array for this facet block plugin. + * + * @return array + * A renderable array representing the content of the block. + * + * @see \Drupal\facets\Plugin\facets\facet_source\SearchApiDisplay + */ + public function buildFacet(); + } diff --git a/src/Plugin/Block/FacetBlock.php b/src/Plugin/Block/FacetBlock.php index 57968fab..9e0d6c20 100644 --- a/src/Plugin/Block/FacetBlock.php +++ b/src/Plugin/Block/FacetBlock.php @@ -5,7 +5,7 @@ namespace Drupal\facets\Plugin\Block; use Drupal\Core\Block\BlockBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Plugin\PluginBase; +use Drupal\Core\Form\FormStateInterface; use Drupal\facets\FacetManager\DefaultFacetManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -82,11 +82,29 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface { // Let the facet_manager build the facets. $build = $this->facetManager->build($facet); - // Add contextual links only when we have results. if (!empty($build)) { + // Add extra elements from facet source, for example, ajax scripts. + // @see Drupal\facets\Plugin\facets\facet_source\SearchApiDisplay + /* @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */ + $facet_source = $facet->getFacetSource(); + $build += $facet_source->buildFacet(); + + // Add contextual links only when we have results. $build['#contextual_links']['facets_facet'] = [ 'route_parameters' => ['facets_facet' => $facet->id()], ]; + + // Add classes needed for ajax. + if (!empty($build['#use_ajax'])) { + $build['#attributes']['class'][] = 'block-facets-ajax'; + // The configuration block id isn't always set in the configuration. + if (isset($this->configuration['block_id'])) { + $build['#attributes']['class'][] = 'js-facet-block-id-' . $this->configuration['block_id']; + } + else { + $build['#attributes']['class'][] = 'js-facet-block-id-' . $this->pluginId; + } + } } return $build; @@ -135,4 +153,14 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface { return ['config' => [$facet->getConfigDependencyName()]]; } + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + // Save block id to configuration, we do this for loading the original block + // with ajax. + $block_id = $form['id']['#value']; + $this->configuration['block_id'] = $block_id; + } + } diff --git a/src/Plugin/facets/facet_source/SearchApiDisplay.php b/src/Plugin/facets/facet_source/SearchApiDisplay.php index 028d3867..78d8bff1 100644 --- a/src/Plugin/facets/facet_source/SearchApiDisplay.php +++ b/src/Plugin/facets/facet_source/SearchApiDisplay.php @@ -5,6 +5,7 @@ namespace Drupal\facets\Plugin\facets\facet_source; use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Core\Extension\ModuleHandler; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; use Drupal\facets\Exception\Exception; use Drupal\facets\Exception\InvalidQueryTypeException; use Drupal\facets\FacetInterface; @@ -372,4 +373,31 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo throw new Exception("Field with name {$field_name} does not have a definition"); } + /** + * {@inheritdoc} + */ + public function buildFacet() { + $build = parent::buildFacet(); + $view = $this->getViewsDisplay(); + if ($view === NULL) { + return $build; + } + + // Add JS for Views with Ajax Enabled. + if ($view->display_handler->ajaxEnabled()) { + $js_settings = [ + 'view_id' => $view->id(), + 'current_display_id' => $view->current_display, + 'view_base_path' => ltrim($view->getPath(), '/'), + 'ajax_path' => Url::fromRoute('views.ajax')->toString(), + ]; + $build['#attached']['library'][] = 'facets/drupal.facets.views-ajax'; + $build['#attached']['drupalSettings']['facets_views_ajax'] = [ + $this->facet->id() => $js_settings, + ]; + $build['#use_ajax'] = TRUE; + } + return $build; + } + } diff --git a/src/Widget/WidgetPluginBase.php b/src/Widget/WidgetPluginBase.php index 032cda86..30b4f63d 100644 --- a/src/Widget/WidgetPluginBase.php +++ b/src/Widget/WidgetPluginBase.php @@ -6,6 +6,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Link; use Drupal\Core\Plugin\PluginBase; +use Drupal\Core\Url; use Drupal\facets\FacetInterface; use Drupal\facets\Result\Result; use Drupal\facets\Result\ResultInterface; @@ -48,6 +49,21 @@ abstract class WidgetPluginBase extends PluginBase implements WidgetPluginInterf return $this->buildResultItem($result); } else { + // When the facet is being build in an AJAX request, and the facetsource + // is a block, we need to update the url to use the current request url. + if ($result->getUrl()->isRouted() && $result->getUrl()->getRouteName() === 'facets.block.ajax') { + $request = \Drupal::request(); + $url_object = \Drupal::service('path.validator') + ->getUrlIfValid($request->getPathInfo()); + if ($url_object) { + $url = $result->getUrl(); + $options = $url->getOptions(); + $route_params = $url_object->getRouteParameters(); + $route_name = $url_object->getRouteName(); + $result->setUrl(new Url($route_name, $route_params, $options)); + } + } + return $this->buildListItems($facet, $result); } }, $facet->getResults()); diff --git a/tests/src/Functional/ProcessorIntegrationTest.php b/tests/src/Functional/ProcessorIntegrationTest.php index ec6f87e9..25b739aa 100644 --- a/tests/src/Functional/ProcessorIntegrationTest.php +++ b/tests/src/Functional/ProcessorIntegrationTest.php @@ -795,7 +795,7 @@ class ProcessorIntegrationTest extends FacetsTestBase { $this->drupalPostForm($this->editForm, $form, 'Save'); $this->drupalGet('search-api-test-fulltext'); - $this->assertSession()->pageTextContains(' Displaying 1 search results'); + $this->assertSession()->pageTextContains('Displaying 1 search results'); $this->assertNoFacetBlocksAppear(); } diff --git a/tests/src/Functional/TestHelperTrait.php b/tests/src/Functional/TestHelperTrait.php index a804654b..e6468137 100644 --- a/tests/src/Functional/TestHelperTrait.php +++ b/tests/src/Functional/TestHelperTrait.php @@ -63,8 +63,13 @@ trait TestHelperTrait { */ protected function assertNoFacetBlocksAppear() { foreach ($this->blocks as $block) { - $this->assertFalse($this->xpath('//div[@id = :id]', [':id' => 'block-' . $block->id()])); - $this->assertSession()->pageTextNotContains($block->label()); + $xpath = $this->xpath('//div[@id = :id]/div[@class="facet-empty"]', [':id' => 'block-' . $block->id()]); + if (!$xpath) { + $this->assertFalse($xpath); + } + else { + $this->assertTrue($this->xpath('//div[@id = :id]/div[@class="facet-empty"]', [':id' => 'block-' . $block->id()])); + } } } diff --git a/tests/src/FunctionalJavascript/AjaxBehaviorTest.php b/tests/src/FunctionalJavascript/AjaxBehaviorTest.php new file mode 100644 index 00000000..1cfa40de --- /dev/null +++ b/tests/src/FunctionalJavascript/AjaxBehaviorTest.php @@ -0,0 +1,69 @@ +<?php + +namespace Drupal\Tests\facets\FunctionalJavascript; + +use Drupal\views\Entity\View; + +/** + * Tests for the JS that powers ajax. + * + * @group facets + */ +class AjaxBehaviorTest extends JsBase { + + /** + * Tests ajax links. + */ + public function testAjaxLinks() { + // Create facets. + $this->createFacet('owl'); + $this->createFacet('duck', 'keywords'); + + // Force ajax. + $view = View::load('search_api_test_view'); + $display = $view->getDisplay('page_1'); + $display['display_options']['use_ajax'] = TRUE; + $view->save(); + + // Go to the views page. + $this->drupalGet('search-api-test-fulltext'); + + // Make sure the blocks are shown on the page. + $page = $this->getSession()->getPage(); + $block_owl = $page->findById('block-owl-block'); + $block_owl->isVisible(); + $block_duck = $page->findById('block-duck-block'); + $block_duck->isVisible(); + $this->assertSession()->pageTextContains('Displaying 5 search results'); + + // Check that the article link exists (and is formatted like a facet) link. + $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']); + $this->assertNotEmpty($links); + + // Click the item facet. + $this->clickLink('item'); + + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->pageTextContains('Displaying 3 search results'); + + // Check that the article facet is now gone. + $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']); + $this->assertEmpty($links); + + // Click the item facet again, and check that the article facet is back. + $this->clickLink('item'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertSession()->pageTextContains('Displaying 5 search results'); + $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'article']); + // $this->assertNotEmpty($links); + + // Check that the strawberry link disappears when filtering on items. + $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'strawberry']); + $this->assertNotEmpty($links); + $this->clickLink('item'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $links = $this->xpath('//a//span[normalize-space(text())=:label]', [':label' => 'strawberry']); + $this->assertEmpty($links); + } + +} diff --git a/tests/src/FunctionalJavascript/JsBase.php b/tests/src/FunctionalJavascript/JsBase.php new file mode 100644 index 00000000..596ed8e2 --- /dev/null +++ b/tests/src/FunctionalJavascript/JsBase.php @@ -0,0 +1,168 @@ +<?php + +namespace Drupal\Tests\facets\FunctionalJavascript; + +use Drupal\block\Entity\Block; +use Drupal\FunctionalJavascriptTests\JavascriptTestBase; +use Drupal\search_api\Entity\Index; + +/** + * Tests for the JS that transforms widgets into form elements. + */ +abstract class JsBase extends JavascriptTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'views', + 'search_api', + 'facets', + 'facets_search_api_dependency', + 'block', + ]; + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + // Create the users used for the tests. + $admin_user = $this->drupalCreateUser([ + 'administer search_api', + 'administer facets', + 'access administration pages', + 'administer blocks', + ]); + $this->drupalLogin($admin_user); + + $this->insertExampleContent(); + } + + /** + * Setup and insert test content. + */ + protected function insertExampleContent() { + entity_test_create_bundle('item', NULL, 'entity_test_mulrev_changed'); + entity_test_create_bundle('article', NULL, 'entity_test_mulrev_changed'); + + $entity_test_storage = \Drupal::entityTypeManager() + ->getStorage('entity_test_mulrev_changed'); + $entity_1 = $entity_test_storage->create([ + 'name' => 'foo bar baz', + 'body' => 'test test', + 'type' => 'item', + 'keywords' => ['orange'], + 'category' => 'item_category', + ]); + $entity_1->save(); + $entity_2 = $entity_test_storage->create([ + 'name' => 'foo test', + 'body' => 'bar test', + 'type' => 'item', + 'keywords' => ['orange', 'apple', 'grape'], + 'category' => 'item_category', + ]); + $entity_2->save(); + $entity_3 = $entity_test_storage->create([ + 'name' => 'bar', + 'body' => 'test foobar', + 'type' => 'item', + ]); + $entity_3->save(); + $entity_4 = $entity_test_storage->create([ + 'name' => 'foo baz', + 'body' => 'test test test', + 'type' => 'article', + 'keywords' => ['apple', 'strawberry', 'grape'], + 'category' => 'article_category', + ]); + $entity_4->save(); + $entity_5 = $entity_test_storage->create([ + 'name' => 'bar baz', + 'body' => 'foo', + 'type' => 'article', + 'keywords' => ['orange', 'strawberry', 'grape', 'banana'], + 'category' => 'article_category', + ]); + $entity_5->save(); + + $inserted_entities = \Drupal::entityQuery('entity_test_mulrev_changed') + ->count() + ->execute(); + $this->assertEquals(5, $inserted_entities, "5 items inserted."); + + /** @var \Drupal\search_api\IndexInterface $index */ + $index = Index::load('database_search_index'); + $indexed_items = $index->indexItems(); + $this->assertEquals(5, $indexed_items, '5 items indexed.'); + } + + /** + * Create and place a facet block in the first sidebar. + * + * @param string $id + * Create a block for a facet. + */ + protected function createBlock($id) { + $config = \Drupal::configFactory(); + $settings = [ + 'plugin' => 'facet_block:' . $id, + 'region' => 'sidebar_first', + 'id' => $id . '_block', + 'theme' => $config->get('system.theme')->get('default'), + 'label' => ucfirst($id) . ' block', + 'visibility' => [], + 'weight' => 0, + ]; + + foreach (['region', 'id', 'theme', 'plugin', 'weight', 'visibility'] as $key) { + $values[$key] = $settings[$key]; + // Remove extra values that do not belong in the settings array. + unset($settings[$key]); + } + $values['settings'] = $settings; + $block = Block::create($values); + $block->save(); + } + + /** + * Create a facet. + * + * @param string $id + * The id of the facet. + * @param string $field + * The field name. + */ + protected function createFacet($id, $field = 'type') { + $facet_storage = \Drupal::entityTypeManager()->getStorage('facets_facet'); + // Create and save a facet with a checkbox widget. + $facet_storage->create([ + 'id' => $id, + 'name' => strtoupper($id), + 'url_alias' => $id, + 'facet_source_id' => 'search_api:views_page__search_api_test_view__page_1', + 'field_identifier' => $field, + 'empty_behavior' => ['behavior' => 'none'], + 'weight' => 1, + 'widget' => [ + 'type' => 'links', + 'config' => [ + 'show_numbers' => TRUE, + 'soft_limit' => 0, + ], + ], + 'processor_configs' => [ + 'url_processor_handler' => [ + 'processor_id' => 'url_processor_handler', + 'weights' => ['pre_query' => -10, 'build' => -10], + 'settings' => [], + ], + ], + 'query_operator' => 'AND', + ])->save(); + $this->createBlock($id); + } + +} diff --git a/tests/src/FunctionalJavascript/WidgetJSTest.php b/tests/src/FunctionalJavascript/WidgetJSTest.php index 2b606a29..0d67d973 100644 --- a/tests/src/FunctionalJavascript/WidgetJSTest.php +++ b/tests/src/FunctionalJavascript/WidgetJSTest.php @@ -2,46 +2,14 @@ namespace Drupal\Tests\facets\FunctionalJavascript; -use Drupal\block\Entity\Block; use Drupal\facets\Entity\Facet; -use Drupal\FunctionalJavascriptTests\JavascriptTestBase; -use Drupal\search_api\Entity\Index; /** * Tests for the JS that transforms widgets into form elements. * * @group facets */ -class WidgetJSTest extends JavascriptTestBase { - - /** - * {@inheritdoc} - */ - public static $modules = [ - 'views', - 'search_api', - 'facets', - 'facets_search_api_dependency', - 'block', - ]; - - /** - * {@inheritdoc} - */ - public function setUp() { - parent::setUp(); - - // Create the users used for the tests. - $admin_user = $this->drupalCreateUser([ - 'administer search_api', - 'administer facets', - 'access administration pages', - 'administer blocks', - ]); - $this->drupalLogin($admin_user); - - $this->insertExampleContent(); - } +class WidgetJSTest extends JsBase { /** * Tests JS interactions in the admin UI. @@ -251,91 +219,4 @@ class WidgetJSTest extends JavascriptTestBase { $this->assertTrue(strpos($current_url, 'search-api-test-fulltext?f%5B0%5D=llama%253Aitem') !== FALSE); } - /** - * Setup and insert test content. - */ - protected function insertExampleContent() { - entity_test_create_bundle('item', NULL, 'entity_test_mulrev_changed'); - entity_test_create_bundle('article', NULL, 'entity_test_mulrev_changed'); - - $entity_test_storage = \Drupal::entityTypeManager() - ->getStorage('entity_test_mulrev_changed'); - $entity_1 = $entity_test_storage->create([ - 'name' => 'foo bar baz', - 'body' => 'test test', - 'type' => 'item', - 'keywords' => ['orange'], - 'category' => 'item_category', - ]); - $entity_1->save(); - $entity_2 = $entity_test_storage->create([ - 'name' => 'foo test', - 'body' => 'bar test', - 'type' => 'item', - 'keywords' => ['orange', 'apple', 'grape'], - 'category' => 'item_category', - ]); - $entity_2->save(); - $entity_3 = $entity_test_storage->create([ - 'name' => 'bar', - 'body' => 'test foobar', - 'type' => 'item', - ]); - $entity_3->save(); - $entity_4 = $entity_test_storage->create([ - 'name' => 'foo baz', - 'body' => 'test test test', - 'type' => 'article', - 'keywords' => ['apple', 'strawberry', 'grape'], - 'category' => 'article_category', - ]); - $entity_4->save(); - $entity_5 = $entity_test_storage->create([ - 'name' => 'bar baz', - 'body' => 'foo', - 'type' => 'article', - 'keywords' => ['orange', 'strawberry', 'grape', 'banana'], - 'category' => 'article_category', - ]); - $entity_5->save(); - - $inserted_entities = \Drupal::entityQuery('entity_test_mulrev_changed') - ->count() - ->execute(); - $this->assertEquals(5, $inserted_entities, "5 items inserted."); - - /** @var \Drupal\search_api\IndexInterface $index */ - $index = Index::load('database_search_index'); - $indexed_items = $index->indexItems(); - $this->assertEquals(5, $indexed_items, '5 items indexed.'); - } - - /** - * Create and place a facet block in the first sidebar. - * - * @param string $id - * Create a block for a facet. - */ - protected function createBlock($id) { - $config = \Drupal::configFactory(); - $settings = [ - 'plugin' => 'facet_block:' . $id, - 'region' => 'sidebar_first', - 'id' => $id . '_block', - 'theme' => $config->get('system.theme')->get('default'), - 'label' => ucfirst($id) . ' block', - 'visibility' => [], - 'weight' => 0, - ]; - - foreach (['region', 'id', 'theme', 'plugin', 'weight', 'visibility'] as $key) { - $values[$key] = $settings[$key]; - // Remove extra values that do not belong in the settings array. - unset($settings[$key]); - } - $values['settings'] = $settings; - $block = Block::create($values); - $block->save(); - } - } -- GitLab