You are here

ajax_facets.module in Ajax facets 7.3

Same filename and directory in other branches
  1. 7 ajax_facets.module
  2. 7.2 ajax_facets.module

File

ajax_facets.module
View source
<?php

/**
 * @file
 *  Ajax facets implementation.
 */
require_once __DIR__ . '/includes/ajax_facets.block.inc';

/**
 * Implements hook_menu().
 */
function ajax_facets_menu() {
  $items = [];
  $items['ajax/ajax_facets/refresh'] = [
    'title' => 'Callback to update facets content',
    'page callback' => 'ajax_facets_refresh_facets_content',
    'access arguments' => [
      'access content',
    ],
    'delivery callback' => 'ajax_deliver',
    'type' => MENU_CALLBACK,
    'file' => 'ajax_facets.pages.inc',
  ];
  return $items;
}

/**
 * Implements hook_theme().
 */
function ajax_facets_theme() {
  return [
    'ajax_facets_ranges_input' => [
      'variables' => [
        'title' => NULL,
        'attributes' => [],
        'value' => NULL,
      ],
      'file' => 'ajax_facets.theme.inc',
    ],
    'ajax_facets_select' => [
      'render element' => 'element',
    ],
  ];
}

/**
 * Implements hook_facetapi_widgets().
 */
function ajax_facets_facetapi_widgets() {
  $query_type = [
    'term',
    'date',
    'date_range',
  ];
  $widgets = [
    // Custom widget to handle ajax-refreshed facets.
    'facetapi_ajax_checkboxes' => [
      'handler' => [
        'label' => 'Ajax multiple checkboxes',
        'class' => 'FacetapiAjaxWidgetCheckboxes',
        'query types' => $query_type,
      ],
    ],
    'facetapi_ajax_select' => [
      'handler' => [
        'label' => 'Ajax selectbox',
        'class' => 'FacetapiAjaxWidgetSelect',
        'query types' => $query_type,
      ],
    ],
    'facetapi_ajax_links' => [
      'handler' => [
        'label' => 'Ajax links',
        'class' => 'FacetapiAjaxWidgetLinks',
        'query types' => $query_type,
      ],
    ],
    'facetapi_ajax_ranges' => [
      'handler' => [
        'label' => 'Ajax ranges',
        'class' => 'FacetapiAjaxWidgetRanges',
        'query types' => [
          'term',
        ],
      ],
    ],
  ];
  return $widgets;
}

/**
 * Implements hook_facetapi_empty_behaviors().
 */
function ajax_facets_facetapi_empty_behaviors() {
  return [
    'ajax_facets' => [
      'handler' => [
        'label' => t('Display ajax_facets wrapper'),
        'class' => 'FacetapiEmptyBehaviorAjaxFacets',
      ],
    ],
  ];
}

/**
 * Add required JS and handle single inclusion.
 */
function ajax_facets_add_ajax_js($facet) {
  static $included = FALSE;
  if (!$included) {
    $included = TRUE;
    $history_js_exists = FALSE;
    $module_path = drupal_get_path('module', 'ajax_facets');
    drupal_add_js($module_path . '/misc/ajax_facets.js');
    drupal_add_js([
      'ajax_facets' => [
        'auto_update_facets' => variable_get('ajax_facets_auto_update_facets', 1),
      ],
    ], [
      'type' => 'setting',
    ]);
    drupal_add_css($module_path . '/misc/ajax_facets.css');
    $search_path = $facet
      ->getAdapter()
      ->getSearchPath();
    $filter_key = $facet
      ->getAdapter()
      ->getUrlProcessor()
      ->getFilterKey();

    // Note that we add in query only filter params and exclude pages and etc...
    $query = isset($_GET[$filter_key]) ? [
      $filter_key => $_GET[$filter_key],
    ] : [];
    $search_results = search_api_current_search();
    $processable_views = [];

    // Process $search_results if we have them.
    if (!empty($search_results)) {

      // Get displays from current search.
      foreach ($search_results as $key => $result) {
        if (substr_count($key, 'search_api_views')) {
          $facetapi_parts = explode(':', $key);
          $views_parts = explode(':', $result[0]
            ->getOption('search id'));
          $processable_views[] = [
            'view_name' => $views_parts[1],
            'view_display_id' => $views_parts[2],
            // Used to identify viewsDomId in settings.facetapi.
            'facetapi_view_display_id' => $facetapi_parts[2],
          ];
        }
      }
    }

    // Add history.js file if exists.
    if (module_exists('libraries')) {
      $history_js_path = libraries_get_path('history.js');
      if ($history_js_path) {
        $history_js_exists = TRUE;
        drupal_add_js($history_js_path . '/scripts/bundled/html4+html5/jquery.history.js', [
          'group' => JS_LIBRARY,
        ]);
      }
    }
    $facet = $facet
      ->getFacet();
    $setting['facetapi'] = [
      'defaultQuery' => isset($_GET[$filter_key]) ? $_GET[$filter_key] : '',
      'searchUrl' => url($search_path),
      'index_id' => $facet['map options']['index id'],
      'views' => $processable_views,
      'facet_field' => $facet['map options']['field']['key'],
      'applyPath' => url($search_path, [
        'query' => $query,
      ]),
      'isHistoryJsExists' => $history_js_exists,
    ];
    drupal_add_js($setting, 'setting');
    drupal_add_library('system', 'drupal.ajax');
  }
}

/**
 * Return Drupal formed url for reset current facet filter.
 */
function ajax_facets_facet_build_reset_path($facet, $adapter) {
  $params = $adapter
    ->getUrlProcessor()
    ->fetchParams();
  $filter_key = $adapter
    ->getUrlProcessor()
    ->getFilterKey();
  $clean_params = [];
  $url_params = [];

  // Build query params except current facet filters.
  if (!empty($params[$filter_key])) {
    foreach ($params[$filter_key] as $param) {
      if (strpos($param, $facet['name']) !== 0) {
        $clean_params[] = $param;
      }
    }
    $url_params = [];
    if (!empty($clean_params)) {
      $url_params = [
        'query' => [
          $filter_key => $clean_params,
        ],
      ];
    }
    $unset_keys = [
      'searchPath',
      'q',
      'page',
      $filter_key,
    ];

    // Remove default params from redirect.
    foreach ($params as $key => $value) {
      if (!in_array($key, $unset_keys)) {
        $url_params['query'][$key] = $value;
      }
    }
  }
  return url(!empty($_GET['searchPath']) ? $_GET['searchPath'] : $adapter
    ->getSearchPath(), $url_params);
}

/**
 * Implementation of hook_views_ajax_data_alter()
 */
function ajax_facets_views_ajax_data_alter(&$commands, $view) {

  // As long as we're on a search api index view
  if (strpos($view->base_table, 'search_api_index') !== FALSE) {

    // We can get the index ID from the view base table
    $index_id = str_replace('search_api_index_', '', $view->base_table);

    // Create the searcher name
    $searcher = 'search_api@' . $index_id;

    // Get our facet blocks
    $blocks = ajax_facets_process_facet_blocks($searcher);

    // Create commands to replace each block. We should use ID's because all facets have it.
    foreach ($blocks['facet_blocks'] as $id => $content) {
      $commands[] = ajax_command_replace("#{$id}-wrapper", $content);
    }

    // Show all blocks
    $commands[] = ajax_command_invoke('div.block--ajax_facets:not(:visible)', 'show');

    // Hide empty blocks
    foreach ($blocks['hide_blocks'] as $block_id) {
      $commands[] = ajax_command_invoke("#{$block_id}", 'hide');
    }

    // Update the views ajax path with the facet query so that exposed filter
    // page requests knows which facets are enabled
    $facet_query = !empty($_GET['f']) ? $_GET['f'] : '';
    if ($facet_query) {
      $settings = [
        'views' => [
          'ajax_path' => url('views/ajax', [
            'query' => [
              'f' => $facet_query,
            ],
          ]),
        ],
      ];

      // We need to put this at the head of the commands so that it runs before
      // the views commands. This is because ajax_render() in ajax.inc prepends
      // it's own settings command to the commands array which will change
      // views ajax_path back to views/ajax. If we don't fix this before views
      // runs it's ajax commands, the views ajax event won't get the facets in
      // the path and they'll be reset on exposed filter input.
      array_unshift($commands, ajax_command_settings($settings, TRUE));
    }
  }
}

/**
 * Generates an array of facet block data for a given searcher and realm
 *
 * @param  string $searcher
 *         The machine name of the searcher.
 *
 * @param  string $realm_name
 *         The machine name of the realm
 *
 * @return array
 *         An array of facet block data
 */
function ajax_facets_process_facet_blocks($searcher, $realm_name = 'block') {
  $map = facetapi_get_delta_map();
  $facets_to_proceed = [];
  $enabled_facets = facetapi_get_enabled_facets($searcher, $realm_name);
  foreach ($enabled_facets as $facet) {
    $facets_to_proceed[] = $facet['name'];
  }

  // Our return array
  $blocks = [
    'facet_blocks' => [],
    'hide_blocks' => [],
    'reset_urls' => [],
    'active_items' => [],
  ];
  $group = $searcher . ':' . $realm_name;
  $realm = facetapi_realm_load($realm_name);

  // Process values once per searcher-realm group.
  $adapter = facetapi_adapter_load($searcher);
  $builds[$group] = facetapi_build_realm($searcher, $realm_name);

  // Take search id.
  $search_id = key(search_api_current_search());

  // Process facets.
  foreach ($facets_to_proceed as $facet_name) {
    $facet = $adapter
      ->getFacet([
      'name' => $facet_name,
    ]);

    //$facet->getSettings($builds[$group])->settings['facet_search_ids']

    //search_api_views:search_results:page

    // First check if the facet is enabled for this search.
    $settings = $facet
      ->getSettings()->settings;
    $default_true = isset($settings['default_true']) ? $settings['default_true'] : TRUE;
    $facet_search_ids = isset($settings['facet_search_ids']) ? $settings['facet_search_ids'] : [];

    // If facet is enabled for this search id.
    // TODO replace it on facetapi_check_block_visibility() ?
    if ($default_true == empty($facet_search_ids[$search_id])) {
      $blocks['reset_urls'][$facet_name] = ajax_facets_facet_build_reset_path($facet, $adapter);
      if (!empty($builds[$group][$facet_name]) && ($build = $facet
        ->getBuild())) {
        $blocks['active_items'][$facet_name] = [];
        foreach ($build as $key => $value) {
          if (!empty($value['#active'])) {
            $blocks['active_items'][$facet_name][] = "{$facet_name}:{$key}";
          }
        }
        if (!empty($blocks['active_items'][$facet_name])) {
          sort($blocks['active_items'][$facet_name]);
        }

        // Skip currently checked facet - we will not refresh them.
        $blocks['facet_blocks'][$builds[$group][$facet_name]['#attributes']['id']] = drupal_render($builds[$group][$facet_name]);
      }
      else {
        $facet_name = rawurlencode($facet_name);
        $delta = array_search("{$searcher}:{$realm_name}:{$facet_name}", $map);
        $blocks['hide_blocks'][] = 'block-facetapi-' . strtolower($delta);
      }
    }
  }
  return $blocks;
}

/**
 * Implements hook_form_form_id_alter().
 */
function ajax_facets_form_facetapi_facet_display_form_alter(&$form, $form_state) {

  // Add JS file for settings form of each facet.
  drupal_add_js(drupal_get_path('module', 'ajax_facets') . '/misc/ajax_facets.admin.js');
}

/**
 * Implements hook_views_pre_render().
 */
function ajax_facets_views_pre_render(&$view) {

  // We use static because we should collect data from all the views.
  static $setting;

  // Save settings of rendered views, to use them in request for AJAX facets.
  $name_display = $base = "{$view->name}:{$view->current_display}";
  $i = 0;

  // Use unique key as in search_api_current_search().
  while (isset($setting['facetapi']['view_args'][$name_display])) {
    $name_display = $base . '-' . ++$i;
  }
  $setting['facetapi']['view_args'][$name_display] = $view->args;
  $setting['facetapi']['exposed_input'][$name_display] = $view->exposed_raw_input;
  $setting['facetapi']['view_path'][$name_display] = $view
    ->get_path();
  $setting['facetapi']['view_dom_id'][$name_display] = $view->dom_id;
  drupal_add_js($setting, 'setting');
}

/**
 * Implements hook_i18n_string_info().
 */
function ajax_facets_i18n_string_info() {
  $groups['ajax_facets'] = [
    'title' => t('Ajax facets'),
    'description' => t('Translatable ajax facets: label.'),
    // This group doesn't have strings with format.
    'format' => FALSE,
    // This group can list all strings.
    'list' => TRUE,
  ];
  return $groups;
}

/**
 * Submit handler for settings form of facet filters.
 */
function ajax_facets_facet_settings_form_submit($form, $form_state) {
  $values = $form_state['values'];

  // Update the i18n strings if need.
  if (function_exists('i18n_string_update')) {
    if (!empty($values['widget'])) {
      if ($values['widget'] == 'facetapi_ajax_select') {

        // Label for the default option of ajax facets select widget..
        $name = [
          'ajax_facets',
          'facet_label',
          str_replace(':', '_', $form['#facetapi']['facet']['name']),
          'label',
        ];
        i18n_string_update($name, $values['ajax_select_default_option_label']);
      }

      // Reset link text.
      if ($values['show_reset_link'] && !empty($values['reset_link_text'])) {

        // Text for the reset link.
        $reset_link_text = [
          'ajax_facets',
          'reset_link_text',
          str_replace(':', '_', $form['#facetapi']['facet']['name']),
          'value',
        ];
        i18n_string_update($reset_link_text, $values['reset_link_text']);
      }
    }
  }
}

/**
 * Fork of function theme_select().
 * Implemented due the issue https://www.drupal.org/node/104715.
 */
function theme_ajax_facets_select($variables) {
  $element = $variables['element'];
  element_set_attributes($element, [
    'id',
    'name',
    'size',
  ]);
  _form_set_class($element, [
    'form-select',
  ]);
  return '<select' . drupal_attributes($element['#attributes']) . '>' . ajax_facets_form_select_options($element) . '</select>';
}

/**
 * Fork of function form_select_options().
 */
function ajax_facets_form_select_options($element, $choices = NULL) {
  if (!isset($choices)) {
    $choices = $element['#options'];
  }

  // array_key_exists() accommodates the rare event where $element['#value'] is NULL.
  // isset() fails in this situation.
  $value_valid = isset($element['#value']) || array_key_exists('#value', $element);
  $value_is_array = $value_valid && is_array($element['#value']);
  $options = '';
  foreach ($choices as $key => $choice) {
    if (is_array($choice)) {
      $options .= '<optgroup label="' . check_plain($key) . '">';
      $options .= form_select_options($element, $choice);
      $options .= '</optgroup>';
    }
    elseif (is_object($choice)) {
      $options .= form_select_options($element, $choice->option);
    }
    else {
      $key = (string) $key;
      if ($value_valid && (!$value_is_array && (string) $element['#value'] === $key || $value_is_array && in_array($key, $element['#value']))) {
        $selected = ' selected="selected"';
      }
      else {
        $selected = '';
      }
      $disabled = !empty($element['#disabled_items'][$key]) ? ' disabled' : '';
      $options .= '<option value="' . check_plain($key) . '"' . $selected . $disabled . '>' . check_plain($choice) . '</option>';
    }
  }
  return $options;
}

Functions

Namesort descending Description
ajax_facets_add_ajax_js Add required JS and handle single inclusion.
ajax_facets_facetapi_empty_behaviors Implements hook_facetapi_empty_behaviors().
ajax_facets_facetapi_widgets Implements hook_facetapi_widgets().
ajax_facets_facet_build_reset_path Return Drupal formed url for reset current facet filter.
ajax_facets_facet_settings_form_submit Submit handler for settings form of facet filters.
ajax_facets_form_facetapi_facet_display_form_alter Implements hook_form_form_id_alter().
ajax_facets_form_select_options Fork of function form_select_options().
ajax_facets_i18n_string_info Implements hook_i18n_string_info().
ajax_facets_menu Implements hook_menu().
ajax_facets_process_facet_blocks Generates an array of facet block data for a given searcher and realm
ajax_facets_theme Implements hook_theme().
ajax_facets_views_ajax_data_alter Implementation of hook_views_ajax_data_alter()
ajax_facets_views_pre_render Implements hook_views_pre_render().
theme_ajax_facets_select Fork of function theme_select(). Implemented due the issue https://www.drupal.org/node/104715.