diff --git a/facets.install b/facets.install index d0a0a22..7f3538b 100644 --- a/facets.install +++ b/facets.install @@ -7,7 +7,6 @@ use Drupal\facets\Entity\Facet; use Drupal\facets\Entity\FacetSource; -use Drupal\block\Entity\Block; /** * Implements hook_update_dependencies(). @@ -174,15 +173,6 @@ 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(); - } + // Empty update hook, we do not support this anymore. + // @see https://www.drupal.org/project/facets/issues/3073444 } diff --git a/facets.libraries.yml b/facets.libraries.yml index 04ce10c..ec44cd5 100644 --- a/facets.libraries.yml +++ b/facets.libraries.yml @@ -74,9 +74,10 @@ soft-limit: - core/drupalSettings drupal.facets.views-ajax: + version: VERSION js: js/facets-views-ajax.js: {} dependencies: + - core/jquery + - views/views.ajax - facets/widget - - core/drupalSettings - - core/drupal.ajax diff --git a/facets.module b/facets.module index 07b5f9d..90eb1ab 100644 --- a/facets.module +++ b/facets.module @@ -15,6 +15,7 @@ use Drupal\facets\Entity\Facet; use Drupal\facets\Entity\FacetSource; use Drupal\facets\FacetInterface; +use Drupal\search_api\Entity\Index; use Drupal\search_api\Query\QueryInterface; use Drupal\views\Entity\View; use Drupal\Core\Entity\EntityInterface; @@ -68,6 +69,11 @@ function facets_theme($existing, $type, $theme, $path) { 'context' => [], ], ], + 'facets_views_plugin' => [ + 'variables' => [ + 'content' => [], + ], + ], ]; } @@ -439,3 +445,23 @@ function facets_theme_suggestions_facets_result_item(array $variables) { } return $suggestions; } + +/** + * Implements hook_views_data_alter(). + */ +function facets_views_data_alter(array &$data) { + + /** @var \Drupal\search_api\IndexInterface $index */ + foreach (Index::loadMultiple() as $index) { + $data['search_api_index_' . $index->id()]['facets'] = [ + 'title' => t('Facets'), + 'help' => t('Displays facets in a filter or area.'), + 'filter' => [ + 'id' => 'facets_filter', + ], + 'area' => [ + 'id' => 'facets_area', + ], + ]; + } +} diff --git a/facets.routing.yml b/facets.routing.yml index 9f26911..1e897e7 100644 --- a/facets.routing.yml +++ b/facets.routing.yml @@ -48,10 +48,3 @@ 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/facets.services.yml b/facets.services.yml index bd4c6fb..e25a4b8 100644 --- a/facets.services.yml +++ b/facets.services.yml @@ -24,6 +24,7 @@ services: - '@plugin.manager.facets.facet_source' - '@plugin.manager.facets.processor' - '@entity_type.manager' + - '@current_route_match' facets.utility.date_handler: class: Drupal\facets\Utility\FacetsDateHandler arguments: @@ -38,3 +39,7 @@ services: arguments: ['@plugin.manager.block'] tags: - { name: event_subscriber } + facets.route_alter: + class: \Drupal\facets\EventSubscriber\RouteAlterSubscriber + tags: + - { name: event_subscriber } diff --git a/js/base-widget.js b/js/base-widget.js index 8ca3e6c..3af9e8b 100644 --- a/js/base-widget.js +++ b/js/base-widget.js @@ -24,7 +24,7 @@ * .once('my-custom-widget-on-change') * .on('change', function () { * // In this example $(this).val() will provide needed URL. - * $(this).trigger('facets_filter', [ $(this).val() ]); + * $(this).trigger('facets_filter', [ $(this).val(), false ]); * }); * * The facets module will trigger "facets_filtering" before filter is diff --git a/js/checkbox-widget.js b/js/checkbox-widget.js index 54b66ef..7b4b3c9 100644 --- a/js/checkbox-widget.js +++ b/js/checkbox-widget.js @@ -67,7 +67,7 @@ var $widget = $(this).closest('.js-facets-widget'); Drupal.facets.disableFacet($widget); - $widget.trigger('facets_filter', [ href ]); + $widget.trigger('facets_filter', [ href, false ]); }); if (active) { diff --git a/js/dropdown-widget.js b/js/dropdown-widget.js index da2e13a..d5b7caa 100644 --- a/js/dropdown-widget.js +++ b/js/dropdown-widget.js @@ -87,7 +87,7 @@ var $linkElement = (anchor.length > 0) ? $(anchor) : $ul.find('.default-option a'); var url = $linkElement.attr('href'); - $(this).trigger('facets_filter', [ url ]); + $(this).trigger('facets_filter', [ url, false ]); }); // Append empty text option. diff --git a/js/facets-views-ajax.js b/js/facets-views-ajax.js index 01e10f1..fd69d95 100644 --- a/js/facets-views-ajax.js +++ b/js/facets-views-ajax.js @@ -1,212 +1,73 @@ /** * @file - * Facets views AJAX handling. + * Facets views Ajax handling. */ - (function ($, Drupal) { - 'use strict'; - /** - * Keep the original beforeSend method to use it later. - */ - var beforeSend = Drupal.Ajax.prototype.beforeSend; + let facetLinks = []; - /** - * Trigger views AJAX refresh on click. - */ - Drupal.behaviors.facetsViewsAjax = { - attach: function (context, settings) { + Drupal.behaviors.FacetsViewsAjax = { + attach: function(context, settings) { + if (settings && settings.views && settings.views.ajaxViews) { + Object.keys(settings.views.ajaxViews || {}).forEach(function (i) { - // 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; - } - }); - } + this.viewsSettings = settings.views.ajaxViews[i]; + let selector = '.js-view-dom-id-' + this.viewsSettings.view_dom_id; + let ajaxPath = settings.views.ajax_path; - if (!view || view.length != 1) { - return; - } + if (ajaxPath.constructor.toString().indexOf('Array') !== -1) { + ajaxPath = ajaxPath[0]; + } - // 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 + ']').each(function (index, facet_item) { - if ($(facet_item).hasClass('js-facets-widget')) { - $(facet_item).unbind('facets_filter.facets'); - $(facet_item).on('facets_filter.facets', function (event, url) { - $('.js-facets-widget').trigger('facets_filtering'); - - updateFacetsView(url, current_dom_id, view_path); - }); + let queryString = window.location.search || ''; + if (queryString !== '') { + queryString = queryString.slice(1).replace(/q=[^&]+&?|&?render=[^&]+/, ''); + if (queryString !== '') { + queryString = (/\?/.test(ajaxPath) ? '&' : '?') + queryString; } + } + + this.element_settings = { + url: ajaxPath + queryString, + submit: settings, + setClick: true, + event: 'click', + selector: selector, + progress: { type: 'fullscreen' } + }; + + $('.facet-item a, .facet-summary-item--facet a').once('facet').each($.proxy(Drupal.attachFacetLinkAjax, this)); + + // Bind on widgets to remove the filter event and take it over. + $('.js-facets-widget').each(function () { + $(this).unbind('facets_filter.facets'); + $(this).on('facets_filter.facets', function (event, url, isLink) { + $('.js-facets-widget').trigger('facets_filtering'); + if (!isLink) { + facetLinks[url].execute(); + } + }); }); - - } - }); - } - }; - - // 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. - var 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) { - href = addExposedFiltersToFacetsUrl(href, options.extraData.view_name, options.extraData.view_display_id); - updateFacetsBlocks(href); - } - } - - // Call the original Drupal method with the right context. - beforeSend.apply(this, arguments); - } - - // Helper function to add exposed form data to facets url - var addExposedFiltersToFacetsUrl = function(href, view_name, view_display_id) { - var $exposed_form = $('form#views-exposed-form-' + view_name.replace(/_/g, '-') + '-' + view_display_id.replace(/_/g, '-')); + Drupal.attachFacetLinkAjax = function(id, link) { - var params = Drupal.Views.parseQueryString(href); + var $link = $(link); + var viewData = {}; + var href = $link.attr('href'); - $.each($exposed_form.serializeArray(), function() { - params[this.name] = this.value; + $.extend(viewData, viewsSettings, Drupal.Views.parseQueryString(href), Drupal.Views.parseViewArgs(href, this.viewsSettings)); + var selfSettings = $.extend({}, this.element_settings, { + submit: viewData, + base: false, + element: link }); - return href.split('?')[0] + '?' + $.param(params); + facetLinks[href] = Drupal.ajax(selfSettings); }; })(jQuery, Drupal); diff --git a/js/link-widget.js b/js/link-widget.js index a96b99c..53c6f8d 100644 --- a/js/link-widget.js +++ b/js/link-widget.js @@ -25,7 +25,7 @@ var clickHandler = function (e) { e.preventDefault(); - $widget.trigger('facets_filter', [$(this).attr('href')]); + $widget.trigger('facets_filter', [$(this).attr('href'), true]); }; // Add correct CSS selector for the widget. The Facets JS API will diff --git a/modules/facets_summary/facets_summary.module b/modules/facets_summary/facets_summary.module index 0a49153..237acac 100644 --- a/modules/facets_summary/facets_summary.module +++ b/modules/facets_summary/facets_summary.module @@ -5,6 +5,7 @@ * Hook implementations for the facets summary module. */ +use Drupal\search_api\Entity\Index; use Drupal\search_api\Query\QueryInterface; /** @@ -125,3 +126,23 @@ function facets_summary_theme_suggestions_facets_summary_item_list(array $variab function facets_summary_preprocess_facets_summary_item_list(array &$variables) { template_preprocess_item_list($variables); } + +/** + * Implements hook_views_data_alter(). + */ +function facets_summary_views_data_alter(array &$data) { + + /** @var \Drupal\search_api\IndexInterface $index */ + foreach (Index::loadMultiple() as $index) { + $data['search_api_index_' . $index->id()]['facets_summary'] = [ + 'title' => t('Facets summary'), + 'help' => t('Displays facets summary in a filter or area'), + 'filter' => [ + 'id' => 'facets_summary_filter', + ], + 'area' => [ + 'id' => 'facets_summary_area', + ], + ]; + } +} diff --git a/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php b/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php index 5b9255a..93ccd99 100644 --- a/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php +++ b/modules/facets_summary/src/Plugin/Block/FacetsSummaryBlock.php @@ -5,7 +5,6 @@ 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; @@ -97,18 +96,6 @@ public function build() { ]; } - /** @var \Drupal\views\ViewExecutable $view */ - if ($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/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php b/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php index 2f41f67..d012600 100644 --- a/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php +++ b/modules/facets_summary/src/Plugin/facets_summary/processor/HideWhenNotRenderedProcessor.php @@ -2,9 +2,12 @@ namespace Drupal\facets_summary\Plugin\facets_summary\processor; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\facets_summary\FacetsSummaryInterface; use Drupal\facets_summary\Processor\BuildProcessorInterface; use Drupal\facets_summary\Processor\ProcessorPluginBase; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a processor that hides the summary when the source was not rendered. @@ -19,14 +22,51 @@ * } * ) */ -class HideWhenNotRenderedProcessor extends ProcessorPluginBase implements BuildProcessorInterface { +class HideWhenNotRenderedProcessor extends ProcessorPluginBase implements BuildProcessorInterface, ContainerFactoryPluginInterface { + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + + /** + * Constructs a new object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The current route match. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteMatchInterface $route_match = NULL) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->routeMatch = $route_match; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_route_match') + ); + } /** * {@inheritdoc} */ public function build(FacetsSummaryInterface $facets_summary, array $build, array $facets) { $facet_source = $facets_summary->getFacetSource(); - if (!$facet_source->isRenderedInCurrentRequest()) { + if (!$facet_source->isRenderedInCurrentRequest() && $this->routeMatch->getRouteName() != 'views.ajax') { return []; } return $build; diff --git a/modules/facets_summary/src/Plugin/views/FacetsSummaryViewsPluginTrait.php b/modules/facets_summary/src/Plugin/views/FacetsSummaryViewsPluginTrait.php new file mode 100644 index 0000000..8f58b36 --- /dev/null +++ b/modules/facets_summary/src/Plugin/views/FacetsSummaryViewsPluginTrait.php @@ -0,0 +1,93 @@ +facetSummaryStorage->loadMultiple(); + + $format = 'search_api:views_%s__%s__%s'; + $source = sprintf($format, $this->view->getDisplay()->getPluginId(), $this->view->id(), $this->view->current_display); + foreach ($facets_summaries as $facets_summary) { + if ($facets_summary->getFacetSourceId() === $source) { + $options[$facets_summary->id()] = $facets_summary->label(); + } + } + + $form['facet_summary'] = [ + '#title' => 'Facet summary', + '#options' => $options, + '#type' => 'radios', + '#required' => TRUE, + '#default_value' => isset($this->options['facet_summary']) ? $this->options['facet_summary'] : [], + ]; + } + + /** + * Gets the facets summary to render. + * + * @return array + * A summary of the facets being used. + */ + public function FacetsViewsGetFacetSummary() { + $build = []; + + /** @var \Drupal\facets_summary\Entity\FacetsSummary $summary */ + $summary = $this->facetSummaryStorage->load($this->options['facet_summary']); + if ($summary) { + $facet_summary = $this->facetSummaryManager->build($summary); + if (!empty($facet_summary)) { + $summary_build = [ + '#theme' => 'block', + '#configuration' => [ + 'provider' => 'facets_summary', + 'label' => $summary->label(), + 'label_display' => TRUE, + ], + '#id' => $summary->id(), + '#plugin_id' => 'facet_summary_block:' . $summary->id(), + '#base_plugin_id' => 'facet_block', + '#derivative_plugin_id' => $summary->id(), + '#weight' => 0, + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => 0, + ], + 'content' => $facet_summary, + ]; + } + } + + if (!empty($summary_build)) { + $build = [ + '#theme' => 'facets_views_plugin', + '#content' => $summary_build, + ]; + + if ($this->view->getDisplay()->ajaxEnabled()) { + $build['#attached']['library'][] = 'facets/drupal.facets.views-ajax'; + } + } + + return $build; + } + +} diff --git a/modules/facets_summary/src/Plugin/views/area/FacetsSummaryArea.php b/modules/facets_summary/src/Plugin/views/area/FacetsSummaryArea.php new file mode 100644 index 0000000..0773387 --- /dev/null +++ b/modules/facets_summary/src/Plugin/views/area/FacetsSummaryArea.php @@ -0,0 +1,103 @@ +facetSummaryManager = $facet_summary_manager; + $this->facetSummaryStorage = $facet_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('facets_summary.manager'), + $container->get('entity_type.manager')->getStorage('facets_summary') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + // Set the default to TRUE so it shows on empty pages by default. + $options['empty']['default'] = TRUE; + $options['facet_summary'] = ['default' => '']; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->FacetsSummaryiewsBuildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function render($empty = FALSE) { + return $this->FacetsViewsGetFacetSummary(); + } + +} diff --git a/modules/facets_summary/src/Plugin/views/filter/FacetsSummaryFilter.php b/modules/facets_summary/src/Plugin/views/filter/FacetsSummaryFilter.php new file mode 100644 index 0000000..3954b08 --- /dev/null +++ b/modules/facets_summary/src/Plugin/views/filter/FacetsSummaryFilter.php @@ -0,0 +1,143 @@ +facetSummaryManager = $facet_summary_manager; + $this->facetSummaryStorage = $facet_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('facets_summary.manager'), + $container->get('entity_type.manager')->getStorage('facets_summary') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $random = new Random(); + $options = parent::defineOptions(); + $options['exposed'] = ['default' => TRUE]; + $options['expose']['contains']['identifier'] = ['default' => 'facet_summary_' . $random->name()]; + $options['facet_summary']['default'] = ''; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->FacetsSummaryiewsBuildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + return ''; + } + + /** + * {@inheritdoc} + */ + public function valueForm(&$form, FormStateInterface $form_state) { + static $is_processing = NULL; + + if ($is_processing) { + $form['value'] = NULL; + return; + } + + $is_processing = TRUE; + $form['value'] = $this->FacetsViewsGetFacetSummary(); + $is_processing = FALSE; + } + + /** + * {@inheritdoc} + */ + public function acceptExposedInput($input) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function validateExposeForm($form, FormStateInterface $form_state) {} + + /** + * {@inheritdoc} + */ + public function canGroup() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function query() {} + +} diff --git a/src/Controller/FacetBlockAjaxController.php b/src/Controller/FacetBlockAjaxController.php deleted file mode 100644 index 8e5613d..0000000 --- a/src/Controller/FacetBlockAjaxController.php +++ /dev/null @@ -1,179 +0,0 @@ -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); - $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/Controller/FacetsViewsAjaxController.php b/src/Controller/FacetsViewsAjaxController.php new file mode 100644 index 0000000..3f60abe --- /dev/null +++ b/src/Controller/FacetsViewsAjaxController.php @@ -0,0 +1,25 @@ +facetsRemoveQueryParams($request); + $response = parent::ajaxView($request); + return $this->addExposedBlockToResponse($response); + } + +} diff --git a/src/Controller/FacetsViewsAjaxGetController.php b/src/Controller/FacetsViewsAjaxGetController.php new file mode 100644 index 0000000..756ff39 --- /dev/null +++ b/src/Controller/FacetsViewsAjaxGetController.php @@ -0,0 +1,25 @@ +facetsRemoveQueryParams($request); + $response = parent::ajaxView($request); + return $this->addExposedBlockToResponse($response); + } + +} diff --git a/src/EventSubscriber/RouteAlterSubscriber.php b/src/EventSubscriber/RouteAlterSubscriber.php new file mode 100644 index 0000000..307398a --- /dev/null +++ b/src/EventSubscriber/RouteAlterSubscriber.php @@ -0,0 +1,39 @@ +getRouteCollection(); + if ($route = $collection->get('views.ajax')) { + $controller = $route->getDefault('_controller'); + // Views ajax get support. + if ($controller == '\Drupal\views_ajax_get\Controller\ViewsAjaxController::ajaxView') { + $route->setDefault('_controller', '\Drupal\facets\Controller\FacetsViewsAjaxGetController::ajaxView'); + } + else { + $route->setDefault('_controller', '\Drupal\facets\Controller\FacetsViewsAjaxController::ajaxView'); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[RoutingEvents::ALTER][] = ['onRouteAlter', -100]; + return $events; + } + +} diff --git a/src/FacetManager/DefaultFacetManager.php b/src/FacetManager/DefaultFacetManager.php index 7053882..ada5702 100644 --- a/src/FacetManager/DefaultFacetManager.php +++ b/src/FacetManager/DefaultFacetManager.php @@ -3,6 +3,7 @@ namespace Drupal\facets\FacetManager; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\facets\Exception\InvalidProcessorException; use Drupal\facets\FacetInterface; @@ -78,6 +79,13 @@ class DefaultFacetManager { */ protected $processedFacets; + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + /** * Constructs a new instance of the DefaultFacetManager. * @@ -89,12 +97,15 @@ class DefaultFacetManager { * The processor plugin manager. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type plugin manager. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The current route match. */ - public function __construct(QueryTypePluginManager $query_type_plugin_manager, FacetSourcePluginManager $facet_source_manager, ProcessorPluginManager $processor_plugin_manager, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(QueryTypePluginManager $query_type_plugin_manager, FacetSourcePluginManager $facet_source_manager, ProcessorPluginManager $processor_plugin_manager, EntityTypeManagerInterface $entity_type_manager, RouteMatchInterface $route_match) { $this->queryTypePluginManager = $query_type_plugin_manager; $this->facetSourcePluginManager = $facet_source_manager; $this->processorPluginManager = $processor_plugin_manager; $this->facetStorage = $entity_type_manager->getStorage('facets_facet'); + $this->routeMatch = $route_match; } /** @@ -253,7 +264,7 @@ public function build(FacetInterface $facet) { // static cache. $facet = $this->facets[$facet->id()]; - if ($facet->getOnlyVisibleWhenFacetSourceIsVisible()) { + if ($facet->getOnlyVisibleWhenFacetSourceIsVisible() && $this->routeMatch->getRouteName() != 'views.ajax') { // Block rendering and processing should be stopped when the facet source // is not available on the page. Returning an empty array here is enough // to halt all further processing. @@ -325,7 +336,7 @@ public function build(FacetInterface $facet) { if ($empty_behavior['behavior'] == 'text') { return [ [ - $build, + 0 => $build, '#type' => 'container', '#attributes' => [ 'data-drupal-facet-id' => $facet->id(), @@ -346,7 +357,7 @@ public function build(FacetInterface $facet) { // content. return [ [ - $build, + 0 => $build, '#type' => 'container', '#attributes' => [ 'data-drupal-facet-id' => $facet->id(), diff --git a/src/FacetsViewsAjaxTrait.php b/src/FacetsViewsAjaxTrait.php new file mode 100644 index 0000000..9125caa --- /dev/null +++ b/src/FacetsViewsAjaxTrait.php @@ -0,0 +1,75 @@ +getMethod() === Request::METHOD_POST) { + foreach (['f', 'page'] as $key) { + if ($request->query->has($key)) { + $request->query->remove($key); + } + } + } + } + + /** + * Adds the exposed form to the response if necessary. + * + * @param $response + * The AJAX response. This is not type hinted because this can either be + * a ViewsAjaxResponse or a CacheableViewsAjaxResponse. + * + * @return mixed + * An empty array, the output from Views, or the response object. + */ + public function addExposedBlockToResponse($response) { + $view = $response->getView(); + $display = $view->getDisplay(); + if ($display->getOption('exposed_block') && $display->usesExposedFormInBlock()) { + + $context = new RenderContext(); + $exposed_block = $this->renderer->executeInRenderContext($context, function () use ($view) { + $output = $view->display_handler->viewExposedFormBlocks(); + if (is_array($output) && !empty($output)) { + return $output; + } + + return []; + }); + + if (!$context->isEmpty() && !empty($exposed_block)) { + $bubbleable_metadata = $context->pop(); + BubbleableMetadata::createFromRenderArray($exposed_block) + ->merge($bubbleable_metadata) + ->applyTo($exposed_block); + } + + // Replace exposed block. + $selector = 'views-exposed-form-' . strtr($view->id(), '_', '-') . '-' . strtr($view->current_display, '_', '-'); + $response->addCommand(new ReplaceCommand("#" . $selector, $exposed_block)); + } + + return $response; + } + +} diff --git a/src/Plugin/Block/FacetBlock.php b/src/Plugin/Block/FacetBlock.php index e7dd781..8a40aa6 100644 --- a/src/Plugin/Block/FacetBlock.php +++ b/src/Plugin/Block/FacetBlock.php @@ -5,7 +5,6 @@ use Drupal\Core\Block\BlockBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Form\FormStateInterface; use Drupal\facets\FacetManager\DefaultFacetManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -100,18 +99,6 @@ public function build() { else { $build['#attributes']['class'][] = 'facet-inactive'; } - - // 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; @@ -160,17 +147,4 @@ public function calculateDependencies() { return ['config' => [$facet->getConfigDependencyName()]]; } - /** - * {@inheritdoc} - */ - public function blockSubmit($form, FormStateInterface $form_state) { - // Checks for a valid form id. Panelizer does not generate one. - if (isset($form['id']['#value'])) { - // 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 7ae0529..c02b159 100644 --- a/src/Plugin/facets/facet_source/SearchApiDisplay.php +++ b/src/Plugin/facets/facet_source/SearchApiDisplay.php @@ -5,7 +5,6 @@ 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; @@ -146,6 +145,39 @@ public function getPath() { return \Drupal::service('path.current')->getPath(); } + /** + * Helper function to get arguments for views contextual filters. + * + * @return array + * Values of contextual filters. + */ + private function extractArgumentsForViewDisplay(): array { + $argumentValues = []; + // For AJAX requests we cannot take the value the same way as for non-AJAX + // requests because route is identified as Drupal AJAX and views arguments + // are removed by Views. + if ($this->request->isXmlHttpRequest()) { + $argumentValues = explode('/', $_REQUEST['view_args']); + } + else { + $display = $this->getViewsDisplay()->getDisplay(); + + // Display plugin which have a path, i.e. pages. + // @see \Drupal\views\Plugin\views\display\PathPluginBase + if ($display->hasPath()) { + $viewUrlParameters = $display->getUrl()->getRouteParameters(); + if (!empty($viewUrlParameters)) { + foreach ($viewUrlParameters as $viewUrlParameter => $validator) { + $argumentValues[] = $this->request->attributes->has($viewUrlParameter) ? $this->request->attributes->get($viewUrlParameter) : NULL; + } + } + } + // @todo Support other plugin types. + } + + return $argumentValues; + } + /** * {@inheritdoc} */ @@ -164,6 +196,7 @@ public function fillFacetsWithResults(array $facets) { if ($results === NULL && isset($display_definition['view_id'])) { $view = Views::getView($display_definition['view_id']); $view->setDisplay($display_definition['view_display']); + $view->setArguments($this->extractArgumentsForViewDisplay()); $view->execute(); $results = $this->searchApiQueryHelper->getResults($search_id); } @@ -383,20 +416,6 @@ public function buildFacet() { 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/Plugin/views/FacetsViewsPluginTrait.php b/src/Plugin/views/FacetsViewsPluginTrait.php new file mode 100644 index 0000000..4566d1b --- /dev/null +++ b/src/Plugin/views/FacetsViewsPluginTrait.php @@ -0,0 +1,96 @@ +facetStorage->loadMultiple(); + + $format = 'search_api:views_%s__%s__%s'; + $source = sprintf($format, $this->view->getDisplay()->getPluginId(), $this->view->id(), $this->view->current_display); + foreach ($facets as $facet) { + if ($facet->getFacetSourceId() === $source) { + $options[$facet->id()] = $facet->label(); + } + } + + $form['facets'] = [ + '#title' => 'Facets', + '#options' => $options, + '#type' => 'checkboxes', + '#required' => TRUE, + '#default_value' => isset($this->options['facets']) ? $this->options['facets'] : [], + ]; + } + + /** + * Gets the facets to render. + * + * @return array + * The facet blocks to be output, in render array format. + */ + public function FacetsViewsGetFacets() { + $build = []; + + /** @var \Drupal\facets\Entity\Facet[] $facets */ + $items = []; + $facets = $this->facetStorage->loadMultiple(array_filter($this->options['facets'])); + foreach ($facets as $facet) { + $facet_build = $this->facetManager->build($facet); + if (!empty($facet_build)) { + $facet_source = $facet->getFacetSource(); + $facet_build += $facet_source->buildFacet(); + $items[] = [ + '#theme' => 'block', + '#configuration' => [ + 'provider' => 'facets', + 'label' => $facet->label(), + 'label_display' => TRUE, + ], + '#id' => $facet->id(), + '#plugin_id' => 'facet_block:' . $facet->id(), + '#base_plugin_id' => 'facet_block', + '#derivative_plugin_id' => $facet->id(), + '#weight' => $facet->getWeight(), + '#cache' => [ + 'contexts' => [], + 'tags' => [], + 'max-age' => 0, + ], + 'content' => $facet_build, + ]; + } + } + + if (!empty($items)) { + $build = [ + '#theme' => 'facets_views_plugin', + '#content' => $items, + ]; + + if ($this->view->getDisplay()->ajaxEnabled()) { + $build['#attached']['library'][] = 'facets/drupal.facets.views-ajax'; + } + } + + return $build; + } + +} diff --git a/src/Plugin/views/area/FacetsArea.php b/src/Plugin/views/area/FacetsArea.php new file mode 100644 index 0000000..026d181 --- /dev/null +++ b/src/Plugin/views/area/FacetsArea.php @@ -0,0 +1,103 @@ +facetManager = $facet_manager; + $this->facetStorage = $facet_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('facets.manager'), + $container->get('entity_type.manager')->getStorage('facets_facet') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + // Set the default to TRUE so it shows on empty pages by default. + $options['empty']['default'] = TRUE; + $options['facets'] = ['default' => []]; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->FacetsViewsBuildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + return implode(', ', array_filter($this->options['facets'])); + } + + /** + * {@inheritdoc} + */ + public function render($empty = FALSE) { + return $this->FacetsViewsGetFacets(); + } + +} diff --git a/src/Plugin/views/filter/FacetsFilter.php b/src/Plugin/views/filter/FacetsFilter.php new file mode 100644 index 0000000..2b7bc24 --- /dev/null +++ b/src/Plugin/views/filter/FacetsFilter.php @@ -0,0 +1,143 @@ +facetManager = $facet_manager; + $this->facetStorage = $facet_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('facets.manager'), + $container->get('entity_type.manager')->getStorage('facets_facet') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $random = new Random(); + $options = parent::defineOptions(); + $options['exposed'] = ['default' => TRUE]; + $options['expose']['contains']['identifier'] = ['default' => 'facet_' . $random->name()]; + $options['facets']['default'] = []; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + $this->FacetsViewsBuildOptionsForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function adminSummary() { + return implode(', ', array_filter($this->options['facets'])); + } + + /** + * {@inheritdoc} + */ + public function valueForm(&$form, FormStateInterface $form_state) { + static $is_processing = NULL; + + if ($is_processing) { + $form['value'] = NULL; + return; + } + + $is_processing = TRUE; + $form['value'] = $this->FacetsViewsGetFacets(); + $is_processing = FALSE; + } + + /** + * {@inheritdoc} + */ + public function acceptExposedInput($input) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function validateExposeForm($form, FormStateInterface $form_state) {} + + /** + * {@inheritdoc} + */ + public function canGroup() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function query() {} + +} diff --git a/templates/facets-views-plugin.html.twig b/templates/facets-views-plugin.html.twig new file mode 100644 index 0000000..ce4f5d0 --- /dev/null +++ b/templates/facets-views-plugin.html.twig @@ -0,0 +1,3 @@ +
+ {{ content }} +