diff --git a/facets.install b/facets.install index d0a0a22..215316d 100644 --- a/facets.install +++ b/facets.install @@ -174,15 +174,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..34a1d4d 100644 --- a/facets.libraries.yml +++ b/facets.libraries.yml @@ -73,10 +73,11 @@ soft-limit: - core/drupal - core/drupalSettings -drupal.facets.views-ajax: +drupal.facets.views-filter-ajax: + version: VERSION js: - js/facets-views-ajax.js: {} + js/facets-views-filter-ajax.js: { } dependencies: - - facets/widget + - core/jquery - core/drupalSettings - - core/drupal.ajax + - views/views.ajax diff --git a/facets.module b/facets.module index 07b5f9d..0414af1 100644 --- a/facets.module +++ b/facets.module @@ -15,6 +15,7 @@ use Drupal\Core\Url; 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_exposed_filter' => [ + 'variables' => [ + 'facets' => [], + ], + ], ]; } @@ -439,3 +445,20 @@ 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.'), + 'filter' => [ + 'id' => 'facets_filter', + ], + ]; + } +} 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..538582d 100644 --- a/facets.services.yml +++ b/facets.services.yml @@ -38,3 +38,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/facets-views-ajax.js b/js/facets-views-ajax.js deleted file mode 100644 index 01e10f1..0000000 --- a/js/facets-views-ajax.js +++ /dev/null @@ -1,212 +0,0 @@ -/** - * @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 + ']').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); - }); - } - }); - - } - }); - } - }; - - // 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, '-')); - - var params = Drupal.Views.parseQueryString(href); - - $.each($exposed_form.serializeArray(), function() { - params[this.name] = this.value; - }); - - return href.split('?')[0] + '?' + $.param(params); - }; - -})(jQuery, Drupal); diff --git a/js/facets-views-filter-ajax.js b/js/facets-views-filter-ajax.js new file mode 100644 index 0000000..1689e2a --- /dev/null +++ b/js/facets-views-filter-ajax.js @@ -0,0 +1,60 @@ +/** + * @file + * Facets views filter Ajax handling. + */ + +(function ($, Drupal) { + + Drupal.behaviors.FacetsFilters = { + attach: function(context, settings) { + if (settings && settings.views && settings.views.ajaxViews) { + Object.keys(settings.views.ajaxViews || {}).forEach(function (i) { + + this.viewsSettings = settings.views.ajaxViews[i]; + let selector = '.js-view-dom-id-' + this.viewsSettings.view_dom_id; + let ajaxPath = settings.views.ajax_path; + + if (ajaxPath.constructor.toString().indexOf('Array') !== -1) { + ajaxPath = ajaxPath[0]; + } + + 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' } + }; + + $('.facets-widget-links a').once('facet').each($.proxy(Drupal.attachFacetLinkAjax, this)); + }); + } + } + }; + + Drupal.attachFacetLinkAjax = function(id, link) { + + var $link = $(link); + var viewData = {}; + var href = $link.attr('href'); + + $.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 + }); + + Drupal.ajax(selfSettings); + }; + +})(jQuery, Drupal); 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..2d29009 --- /dev/null +++ b/src/Controller/FacetsViewsAjaxController.php @@ -0,0 +1,21 @@ +getFacetsResponse($response); + } + +} diff --git a/src/Controller/FacetsViewsAjaxGetController.php b/src/Controller/FacetsViewsAjaxGetController.php new file mode 100644 index 0000000..2026d5e --- /dev/null +++ b/src/Controller/FacetsViewsAjaxGetController.php @@ -0,0 +1,22 @@ +getFacetsResponse($response); + } + +} diff --git a/src/EventSubscriber/RouteAlterSubscriber.php b/src/EventSubscriber/RouteAlterSubscriber.php new file mode 100644 index 0000000..b953c67 --- /dev/null +++ b/src/EventSubscriber/RouteAlterSubscriber.php @@ -0,0 +1,33 @@ +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/FacetsViewsAjaxTrait.php b/src/FacetsViewsAjaxTrait.php new file mode 100644 index 0000000..16cc34c --- /dev/null +++ b/src/FacetsViewsAjaxTrait.php @@ -0,0 +1,42 @@ +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..ff11434 100644 --- a/src/Plugin/Block/FacetBlock.php +++ b/src/Plugin/Block/FacetBlock.php @@ -100,18 +100,6 @@ class FacetBlock extends BlockBase implements ContainerFactoryPluginInterface { 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; diff --git a/src/Plugin/facets/facet_source/SearchApiDisplay.php b/src/Plugin/facets/facet_source/SearchApiDisplay.php index 7ae0529..883073d 100644 --- a/src/Plugin/facets/facet_source/SearchApiDisplay.php +++ b/src/Plugin/facets/facet_source/SearchApiDisplay.php @@ -383,20 +383,6 @@ class SearchApiDisplay extends FacetSourcePluginBase implements SearchApiFacetSo 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/filter/FacetsFilter.php b/src/Plugin/views/filter/FacetsFilter.php new file mode 100644 index 0000000..9efb4f7 --- /dev/null +++ b/src/Plugin/views/filter/FacetsFilter.php @@ -0,0 +1,149 @@ + TRUE]; + $options['expose']['contains']['identifier'] = ['default' => user_password()]; + $options['facets']['default'] = []; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + + $options = []; + /** @var \Drupal\facets\Entity\Facet[] $facets */ + $facets = \Drupal::entityTypeManager()->getStorage('facets_facet')->loadMultiple(); + + // TODO cleaner detection + $source = 'search_api:views_' . $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'] : [], + ]; + + return $form; + } + + /** + * {@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; + $manager = \Drupal::service('facets.manager'); + + /** @var \Drupal\facets\Entity\Facet[] $facets */ + $items = []; + $facets = \Drupal::entityTypeManager()->getStorage('facets_facet')->loadMultiple(array_filter($this->options['facets'])); + foreach ($facets as $facet) { + $b = $manager->build($facet); + if (!empty($b)) { + $facet_source = $facet->getFacetSource(); + $b += $facet_source->buildFacet(); + unset($b[0]['#attributes']['class']); + $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' => $b + ]; + } + } + + $build = [ + '#theme' => 'facets_exposed_filter', + '#facets' => $items, + ]; + + // TODO only expose when ajax is enabled. For some reason using + // isAjaxEnabled only half works, not sure why yet. The library has a + // dependency on ajax_views.js so we really need to get this right. + $build['#attached']['library'][] = 'facets/drupal.facets.views-filter-ajax'; + + $form['value'] = $build; + + $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-exposed-filter.html.twig b/templates/facets-exposed-filter.html.twig new file mode 100644 index 0000000..7fc9971 --- /dev/null +++ b/templates/facets-exposed-filter.html.twig @@ -0,0 +1,3 @@ +
+ {{ facets }} +