diff --git a/README.md b/README.md index 04bdefec5..2bace85aa 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,22 @@ On each site included in the federated search, you will need to: In order to display results from the Solr index: 1. Configure the application route and settings at `/admin/config/search/federated-search-settings` -2. Set permissions for `Use Federated Search` and `Administer Federated Search` for the proper roles. -3. Optional: [Theme the ReactJS search app](https://www.drupal.org/docs/7/modules/search-api-federated-solr/search-api-federated-solr-module/theming-the-reactjs-search) -4. Optional: Add the federated search page form block to your site theme +1. Set permissions for `Use Federated Search` and `Administer Federated Search` for the proper roles. +1. Optional: Configure default fields for queries. The default query field for search queries made through the proxy provided by this module is the `rendered_item` field. To set a different value for the default query fields there are two options: + 1. Set `$config['search_api_federated_solr_proxy_query_fields']` to an array of _Fulltext_ field machine names (i.e. `['rendered_item', 'full_text_title']`) from your search index in `settings.php`. + - This method will not work if you disable the proxy that this module provides for querying your solr backend in the search app or block autocomplete settings + - By default, the proxy will validate the field names to ensure that they are full text and that they exist on the index for this site. Then it will translate the index field name into its solr field name counterpart. If you need to disable this validation + transformation (for example to search fields on a D8 site index whose machine names are different than the D7 site counterpart), set `$config['search_api_federated_solr_proxy_validate_query_fields_against_schema']` to `FALSE`. Then you must supply the _solr field names_. To determine what these field names are on your D7 site, use the drush command `drush sapifs-f`, which will output a table with index field names and their solr field name counterparts. + 1. Configure that Search API server to set default query fields for your default [Request Handler](https://lucene.apache.org/solr/guide/6_6/requesthandlers-and-searchcomponents-in-solrconfig.html#RequestHandlersandSearchComponentsinSolrConfig-SearchHandlers). (See [example](https://github.com/palantirnet/federated-search-demo/blob/master/conf/solr/drupal8/custom/solr-conf/4.x/solrconfig_extra.xml#L94) in Federated Search Demo site Solr server config) +1. Optional: [Theme the ReactJS search app](https://www.drupal.org/docs/7/modules/search-api-federated-solr/search-api-federated-solr-module/theming-the-reactjs-search) +1. Optional: Add the federated search page form block to your site theme + +## Adding Solr query debug information to proxy response + +To see debug information when using the proxy for your search queries, set `$conf['search_api_federated_solr_proxy_debug_query']` to `TRUE` in your settings.php. + +Then user your browsers developer tools to inspect network traffic. When your site makes a search query through the proxy, inspect the response for this request and you should now see a `debug` object added to the response object. + +*Note: we recommend leaving this set to `FALSE` for production environments, as it could have an impact on performance.* ## Updating the bundled React application diff --git a/css/search_api_federated_solr_autocomplete.css b/css/search_api_federated_solr_autocomplete.css new file mode 100644 index 000000000..fffa8a2de --- /dev/null +++ b/css/search_api_federated_solr_autocomplete.css @@ -0,0 +1,83 @@ +#autocomplete-wrapper { + position: relative; +} + +#federated-search-page-block-form .search-autocomplete-container { + position: absolute; + border: 1px solid #999; + background: #fff; + z-index: 1000; + width: 100%; + display: block; +} + +#federated-search-page-block-form .search-autocomplete-container:not(.visually-hidden) .search-autocomplete-container__title { + font-size:.8em; + line-height:1.46667em; + position: relative; + font-weight: bold; + padding: 10px 20px; + border-bottom: 1px dashed #ccc; + display: block; +} +.search-autocomplete-container:not(.visually-hidden) .search-autocomplete-container__close-button { + font-size:.8em; + line-height:1.46667em; + font-style:normal; + padding:3px 7px; + border-color:#ccc; + color:#333; + cursor:pointer; + position:absolute; + right:5px +} + +.search-autocomplete-container:not(.visually-hidden) .search-autocomplete-container__close-button:hover { + border-color:#b5b5b5; + background-color:#f6f6f6 +} + +#federated-search-page-block-form .search-autocomplete-container:not(.visually-hidden) .search-autocomplete-container__directions { + display: block; + padding:10px 20px; +} + +#federated-search-page-block-form .search-autocomplete-container:not(.visually-hidden) .search-autocomplete-container__directions-item { + display: block; + font-size:.8em; + line-height:1.46667em; +} + +.search-autocomplete-container:not(.visually-hidden) .autocomplete-suggestion { + cursor:pointer; + background-color:#fff; + white-space: nowrap; +} + +.search-autocomplete-container:not(.visually-hidden) .autocomplete-suggestion__link { + display: block; + color:#737373; + text-decoration: none; + font-size:.8em; + line-height:1.46667em; + padding:15px 20px; + border:1px solid #fff; + border-bottom-color:#ccc; +} + +.search-autocomplete-container:not(.visually-hidden) .highlight .autocomplete-suggestion__link, +.search-autocomplete-container:not(.visually-hidden) .autocomplete-suggestion__link:hover { + text-decoration: underline; + background-color:#f6f6f6; + border:1px solid #f6f6f6; + border-bottom-color:#ccc; +} + +.autocomplete-selected { + background: #f0f0f0; +} + +.visually-hidden { + position: absolute !important; + clip: rect(1px 1px 1px 1px); +} diff --git a/js/search_api_federated_solr_autocomplete.js b/js/search_api_federated_solr_autocomplete.js new file mode 100644 index 000000000..fb485e80d --- /dev/null +++ b/js/search_api_federated_solr_autocomplete.js @@ -0,0 +1,484 @@ +/** + * @file + * Adds autocomplete functionality to search_api_solr_federated block form. + */ +(function($) { + var autocomplete = {}; + + /** + * Polyfill for Object.assign + * @see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill + */ + if (typeof Object.assign != 'function') { + // Must be writable: true, enumerable: false, configurable: true + Object.defineProperty(Object, "assign", { + value: function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }, + writable: true, + configurable: true + }); + } + + /** + * Attaches our custom autocomplete settings to the search_api_federated_solr block search form field. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches the autocomplete behaviors. + */ + Drupal.behaviors.searchApiFederatedSolrAutocomplete = { + attach: function attach(context) { + // Find our fields with autocomplete settings + $(context) + .find(".js-search-api-federated-solr-block-form-autocomplete") + .once("search-api-federated-solr-autocomplete-search") + .each(function() { + // Halt execution if we don't have the required config. + if ( + !Object.hasOwnProperty.call( + Drupal.settings, + "searchApiFederatedSolr" + ) || + !Object.hasOwnProperty.call( + Drupal.settings.searchApiFederatedSolr, + "block" + ) || + !Object.hasOwnProperty.call( + Drupal.settings.searchApiFederatedSolr.block, + "autocomplete" + ) || + !Object.hasOwnProperty.call( + Drupal.settings.searchApiFederatedSolr.block.autocomplete, + "url" + ) + ) { + return; + } + // Set default settings. + var defaultSettings = { + isEnabled: false, + appendWildcard: false, + userpass: "", + numChars: 2, + suggestionRows: 5, + mode: "result", + result: { + titleText: "What are you looking for?", + hideDirectionsText: 0 + } + }; + // Get passed in config from block config. + var config = Drupal.settings.searchApiFederatedSolr.block.autocomplete; + // Merge defaults with passed in config. + var options = Object.assign({}, defaultSettings, config); + + // Set scaffolding markup for suggestions container + var suggestionsContainerScaffoldingMarkup = '
'.concat( + options[options.mode].titleText, + '
'); + + if (!options[options.mode].hideDirectionsText) { + suggestionsContainerScaffoldingMarkup += + '
Press ENTER to search for your current term or ESC to close.Press \u2191 and \u2193 to highlight a suggestion then ENTER to be redirected to that suggestion.
'; + } + + suggestionsContainerScaffoldingMarkup += '
'; + + // Cache selectors. + var $input = $(this); + var $form = $("#federated-search-page-block-form"); + + // Set up input with attributes, suggestions scaffolding. + $input.attr("role", "combobox").attr("aria-owns", "res").attr("aria-autocomplete", "list").attr("aria-expanded", "false"); + $(suggestionsContainerScaffoldingMarkup).insertAfter($input); + + // Cache inserted selectors. + var $results = $("#res"); + var $autocompleteContainer = $(".js-search-autocomplete-container"); + var $closeButton = $( + ".js-search-autocomplete-container__close-button" + ); + var $ariaLive = $("#search-api-federated-solr-autocomplete-aria-live"); + + // Initiate helper vars. + var current; + var counter = 1; + var keys = { + ESC: 27, + TAB: 9, + RETURN: 13, + UP: 38, + DOWN: 40 + }; + + // Determine param values for any set default filters/facets. + var defaultParams = ""; + $('input[type="hidden"]', $form).each(function(index, input) { + var fq = $(input).attr("name") + ':("' + $(input).val() + '")'; + defaultParams += "&fq=" + encodeURI(fq); + }); + + // Set the text default query. + var urlWithDefaultParams = options.url + defaultParams; + + // Bind events to input. + $input.bind("input", function(event) { + doSearch(options.suggestionRows); + }); + + $input.bind("keydown", function(event) { + doKeypress(keys, event); + }); + + // Define event handlers. + function doSearch(suggestionRows) { + $input.removeAttr("aria-activedescendant"); + var value = $input.val(); + // Remove spaces on either end of the value. + var trimmed = value.trim(); + // Default to the trimmed value. + var query = trimmed; + // If the current value has more than the configured number of characters. + if (query.length > options.numChars) { + // Append wildcard to the query if configured to do so. + if (options.appendWildcard) { + // Note: syntax for wildcard query depends on the query endpoint + if (options.proxyIsDisabled) { + // One method of supporting search-as-you-type is to append a wildcard '*' + // to match zero or more additional characters at the end of the users search term. + // @see: https://lucene.apache.org/solr/guide/6_6/the-standard-query-parser.html#TheStandardQueryParser-WildcardSearches + // @see: https://opensourceconnections.com/blog/2013/06/07/search-as-you-type-with-solr/ + // Split into word chunks. + var words = trimmed.split(" "); + + // If there are multiple chunks, join them with "+", repeat the last word + append "*". + if (words.length > 1) { + query = words.join("+") + words.pop() + "*"; + } + else { + // If there is only 1 word, repeat it an append "*". + query = words + "+" + words + "*"; + } + } + else { + query = trimmed + "*"; + } + } + + // Replace the placeholder with the query value. + var url = urlWithDefaultParams.replace(/(\[val\])/gi, query); + + // Set up basic auth if we need it. + var xhrFields = {}; + var headers = {}; + + if (options.userpass) { + xhrFields = { + withCredentials: true + }; + headers = { + Authorization: "Basic " + options.userpass + }; + } + + // Make the ajax request + $.ajax({ + xhrFields: xhrFields, + headers: headers, + url: url, + dataType: "json", + success: function success(results) { + // Currently we only support the response structure from Solr: + // { + // response: { + // docs: [ + // { + // ss_federated_title: , + // ss_url: , + // } + // ] + // } + // } + // @todo provide hook for transform function to be passed in + // via Drupal.settings then all it here. + if (results.response.docs.length >= 1) { + // Remove all suggestions + $(".js-autocomplete-suggestion").remove(); + $autocompleteContainer.removeClass("visually-hidden"); + $("#search-autocomplete").append(""); + $input.attr("aria-expanded", "true"); + counter = 1; + + // Bind click event for close button + $closeButton.bind("click", function(event) { + event.preventDefault(); + event.stopPropagation(); + $input.removeAttr("aria-activedescendant"); + + // Remove all suggestions + $(".js-autocomplete-suggestion").remove(); + $autocompleteContainer.addClass("visually-hidden"); + $input.attr("aria-expanded", "false"); + $input.focus(); + + // Emit a custom events for removing. + $(document).trigger("SearchApiFederatedSolr::block::autocomplete::suggestionsRemoved", [{}]); + }); + + // Get first [suggestionRows] results + var limitedResults = results.response.docs.slice(0, suggestionRows); + limitedResults.forEach(function(item) { + // Highlight query chars in returned title + var pattern = new RegExp(trimmed, "gi"); + var highlighted = item.ss_federated_title.replace( + pattern, + function(string) { + return "" + string + ""; + } + ); + // Default the URL to the passed ss_url. + var href = item.ss_url; + // Ensure that the result returned for the item from solr + // (via proxy or directly) is assigned an absolute URL. + if (!options.directUrl) { + // Initialize url to compute from solr sm_urls array. + var sm_url; + // Use the canonical url. + if (Array.isArray(item.sm_urls)) { + sm_url = item.sm_urls[0]; + } + // If no valid urls are passed from solr, skip this item. + if (!sm_url) { + return; + } + // Override the current href value. + href = sm_url; + } + //Add results to the list + var $suggestionTemplate = "
") + .concat( + highlighted, + "(" + ) + .concat(counter, " of ") + .concat( + limitedResults.length, + ")
" + ); + $results.append($suggestionTemplate); + counter = counter + 1; + }); + + // On link click, emit an event whose data can be used to write to analytics, etc. + $(".js-autocomplete-suggestion__link").bind("click", + function(e) { + $(document).trigger("SearchApiFederatedSolr::block::autocomplete::selection", + [ + { + referrer: $(location).attr("href"), + target: $(this).attr("href"), + term: $input.val() + } + ] + ); + } + ); + // Emit a custom events for results. + $(document).trigger("SearchApiFederatedSolr::block::autocomplete::suggestionsLoaded", [{}]); + // Announce the number of suggestions. + var number = $results.children('[role="option"]').length; + if (number >= 1) { + $ariaLive.text(number + " suggestions displayed. To navigate use up and down arrow keys."); + } + } + else { + // No results, remove suggestions and hide container + $(".js-autocomplete-suggestion").remove(); + $autocompleteContainer.addClass("visually-hidden"); + $input.attr("aria-expanded", "false"); // Emit a custom events for removing. + + $(document).trigger( + "SearchApiFederatedSolr::block::autocomplete::suggestionsRemoved", + [{}] + ); + } + }, + error: function error(jqXHR, textStatus, errorThrown) { + // No results, remove suggestions and hide container + $(".js-autocomplete-suggestion").remove(); + $autocompleteContainer.addClass("visually-hidden"); + $input.attr("aria-expanded", "false"); // Emit a custom events for removing. + + $(document).trigger( + "SearchApiFederatedSolr::block::autocomplete::suggestionsRemoved", + [{}] + ); + } + }); + } + else { + // Remove suggestions and hide container + $(".js-autocomplete-suggestion").remove(); + $autocompleteContainer.addClass("visually-hidden"); + $input.attr("aria-expanded", "false"); // Emit a custom events for removing. + + $(document).trigger( + "SearchApiFederatedSolr::block::autocomplete::suggestionsRemoved", + [{}] + ); + } + } + + function doKeypress(keys, event) { + var $suggestions = $(".js-autocomplete-suggestion"); + var highlighted = false; + highlighted = $results.children("div").hasClass("highlight"); + + switch (event.which) { + case keys.ESC: + event.preventDefault(); + event.stopPropagation(); + $input.removeAttr("aria-activedescendant"); + $suggestions.remove(); + $autocompleteContainer.addClass("visually-hidden"); + $input.attr("aria-expanded", "false"); + break; + + case keys.TAB: + $input.removeAttr("aria-activedescendant"); + $suggestions.remove(); + $autocompleteContainer.addClass("visually-hidden"); + $input.attr("aria-expanded", "false"); + break; + + case keys.RETURN: + if (highlighted) { + event.preventDefault(); + event.stopPropagation(); + return selectOption(highlighted, $(".highlight").find("a").attr("href")); + } + else { + $form.submit(); + return false; + } + + break; + + case keys.UP: + event.preventDefault(); + event.stopPropagation(); + return moveUp(highlighted); + break; + + case keys.DOWN: + event.preventDefault(); + event.stopPropagation(); + return moveDown(highlighted); + break; + + default: + return; + } + } + + function moveUp(highlighted) { + $input.removeAttr("aria-activedescendant"); + var ariaLiveText = ''; + // if highlighted exists and if the highlighted item is not the first option + if (highlighted && !$results.children().first("div").hasClass("highlight")) { + removeCurrent(); + current.prev("div").addClass("highlight").attr("aria-selected", true); + $input.attr("aria-activedescendant", current.prev("div").attr("id")); + ariaLiveText = current.prev("div").text(); + $ariaLive.text(ariaLiveText); + } + else { + // Go to bottom of list + removeCurrent(); + current = $results.children().last("div"); + current.addClass("highlight").attr("aria-selected", true); + $input.attr("aria-activedescendant", current.attr("id")); + ariaLiveText = current.text(); + $ariaLive.text(ariaLiveText); + } + } + + function moveDown(highlighted) { + $input.removeAttr("aria-activedescendant"); + var ariaLiveText = ''; + // if highlighted exists and if the highlighted item is not the last option + if (highlighted && !$results.children().last("div").hasClass("highlight")) { + removeCurrent(); + current.next("div").addClass("highlight").attr("aria-selected", true); + $input.attr("aria-activedescendant", current.next("div").attr("id")); + ariaLiveText = current.next("div").text(); + $ariaLive.text(ariaLiveText); + } + else { + // Go to top of list + removeCurrent(); + current = $results.children().first("div"); + current.addClass("highlight").attr("aria-selected", true); + $input.attr("aria-activedescendant", current.attr("id")); + ariaLiveText = current.text(); + $ariaLive.text(ariaLiveText); + } + } + + function removeCurrent() { + current = $results.find(".highlight"); + current.attr("aria-selected", false); + current.removeClass("highlight"); + } + + function selectOption(highlighted, href) { + if (highlighted && href) { + // @todo add logic for non-link suggestions + // Emit an event whose data can be used to write to analytics, etc. + $(document).trigger("SearchApiFederatedSolr::block::autocomplete::selection", + [ + { + referrer: $(location).attr("href"), + target: href, + term: $input.val() + } + ] + ); + // Redirect to the selected link. + $(location).attr("href", href); + } + else { + return; + } + } + }); + } + }; + Drupal.SearchApiFederatedSolrAutocomplete = autocomplete; +})(jQuery); diff --git a/search_api_federated_solr.admin.inc b/search_api_federated_solr.admin.inc new file mode 100644 index 000000000..ee0064c91 --- /dev/null +++ b/search_api_federated_solr.admin.inc @@ -0,0 +1,521 @@ +machine_name] = $index->name; + $settings[$index->machine_name] = $index->options; + } + + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + + $form['setup'] = [ + '#type' => 'fieldset', + '#title' => 'Search Results Page > Set Up', + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ]; + + $form['setup']['search_api_federated_solr_path'] = [ + '#type' => 'textfield', + '#title' => t('Search app path'), + '#default_value' => variable_get('search_api_federated_solr_path', 'search-app'), + '#description' => t('The path for the search app (Default: "search-app").'), + ]; + + $form['setup']['search_api_federated_solr_search_index'] = [ + '#type' => 'select', + '#title' => t('Search API index'), + '#description' => t('Defines which search_api index and server the search app should use.'), + '#options' => $indexes, + '#default_value' => variable_get('search_api_federated_solr_search_index'), + '#required' => TRUE, + '#ajax' => [ + 'callback' => 'get_site_name', + 'wrapper' => 'search-api-federated-solr-config-form', + ], + ]; + + $form['setup']['search_api_federated_solr_disable_query_proxy'] = [ + '#type' => 'checkbox', + '#title' => '' . t('Do not use the proxy for the search query') . '', + '#default_value' => variable_get('search_api_federated_solr_disable_query_proxy'), + '#description' => t('Check this box to configure the search app to query the Solr server directly. When checked, it is highly recommended that you also procure and configure read-only basic auth credentials for the search app. When unchecked, this site will act as a proxy for requests to the Solr server of the chosen Search API index using the Drupal route defined by this module.

Note: Acquia Search customers must leave this box unchecked.'), + '#attributes' => [ + 'data-direct-query-enabler' => TRUE, + ], + ]; + + $form['setup']['search_api_federated_solr_search_index_basic_auth'] = [ + '#type' => 'fieldset', + '#title' => t('Search Index Basic Authentication'), + '#description' => t('If your Solr server is protected by basic HTTP authentication, enter the login data here. These credentials will be accessible to the client in an obscured, but non-secure method. It should, therefore, only provide read access to the index AND be different from that provided when configuring the server in Search API. The Password field is intentionally not obscured to emphasize this distinction.'), + '#states' => [ + 'visible' => [ + ':input[data-direct-query-enabler]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + $form['setup']['search_api_federated_solr_search_index_basic_auth']['search_api_federated_solr_search_index_basic_auth_username'] = [ + '#type' => 'textfield', + '#title' => t('Username'), + '#default_value' => variable_get('search_api_federated_solr_search_index_basic_auth_username'), + '#states' => [ + 'visible' => [ + ':input[data-direct-query-enabler]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + $form['setup']['search_api_federated_solr_search_index_basic_auth']['search_api_federated_solr_search_index_basic_auth_password'] = [ + '#type' => 'textfield', + '#title' => t('Password'), + '#default_value' => variable_get('search_api_federated_solr_search_index_basic_auth_password'), + '#states' => [ + 'visible' => [ + ':input[data-direct-query-enabler]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + /** + * Search results page options: + * - show empty search results (i.e. filterable listing page), + * - customize "no results" text + * - custom search prompt + * - renders in result area when show empty results no enabled and no query value + * - max number of search results per page + * - max number of "numbered" pagination buttons to show + */ + + $form['search_page_options'] = [ + '#type' => 'fieldset', + '#title' => 'Search Results Page > Options', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ]; + + $form['search_page_options']['search_api_federated_solr_show_empty_search_results'] = [ + '#type' => 'checkbox', + '#title' => t('Show results for empty search'), + '#default_value' => variable_get('search_api_federated_solr_show_empty_search_results'), + '#description' => t('When checked, this option allows users to see all results when no search term is entered. By default, empty searches are disabled and yield no results.'), + ]; + + $form['search_page_options']['search_api_federated_solr_no_results_text'] = [ + '#type' => 'textfield', + '#title' => t('No results text'), + '#default_value' => variable_get('search_api_federated_solr_no_results_text'), + '#description' => t('This text is shown when a query returns no results. (Default: "Your search yielded no results.")'), + ]; + + $form['search_page_options']['search_api_federated_solr_search_prompt_text'] = [ + '#type' => 'textfield', + '#title' => t('Search prompt text'), + '#default_value' => variable_get('search_api_federated_solr_search_prompt_text'), + '#description' => t('This text is shown when no query term has been entered. (Default: "Please enter a search term.")'), + ]; + + $form['search_page_options']['search_api_federated_solr_rows'] = [ + '#type' => 'textfield', + '#attributes' => array( + ' type' => 'number', + ), + '#title' => t('Number of search results per page'), + '#default_value' => variable_get('search_api_federated_solr_rows'), + '#description' => t('The max number of results to render per search results page. (Default: 20)'), + ]; + + $form['search_page_options']['search_api_federated_solr_page_buttons'] = [ + '#type' => 'textfield', + '#attributes' => array( + ' type' => 'number', + ), + '#title' => t('Number of pagination buttons'), + '#default_value' => variable_get('search_api_federated_solr_page_buttons'), + '#description' => t('The max number of numbered pagination buttons to show at a given time. (Default: 5)'), + ]; + + /** + * Settings and values for search facets and filters: + * - set the site name facet to the current site name property + */ + + $form['search_form_values'] = [ + '#type' => 'fieldset', + '#title' => 'Search Results Page > Facets & Filters', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ]; + + /** + * Set hidden form element value based on presence of field properties on + * the selected index. This value will determine which inputs are + * visible for setting default facet/filter values and hiding in the UI. + */ + $form['search_form_values']['defaults'] = [ + '#type' => 'fieldset', + '#title' => 'Set facet / filter default values' + ]; + + $form['search_form_values']['defaults']['search_api_federated_solr_set_search_site'] = [ + '#type' => 'checkbox', + '#title' => t('Set the "Site name" facet to this site'), + '#default_value' => variable_get('search_api_federated_solr_set_search_site'), + '#description' => t('When checked, only search results from this site will be shown, by default, until this site\'s checkbox is unchecked in the search app\'s "Site name" facet.
This feature works best when the proxy server is enabled.'), + '#states' => [ + 'visible' => [ + ':input[name="search_api_federated_solr_has_site_name_property"]' => [ + 'value' => "true" + ], + ], + ], + ]; + + if (!empty(variable_get('search_api_federated_solr_search_index'))) { + $index = variable_get('search_api_federated_solr_search_index'); + if (isset($settings[$index])) { + $fields = $settings[$index]['fields']; + } + } + + $form['search_form_values']['search_api_federated_solr_has_site_name_property'] = [ + '#type' => 'hidden', + '#attributes' => [ + 'id' => ['site-name-property'], + ], + '#default_value' => isset($fields['site_name']) ? 'true' : variable_get('search_api_federated_solr_has_site_name_property'), + ]; + + $form['search_form_values']['search_api_federated_solr_has_federated_date_property'] = [ + '#type' => 'hidden', + '#attributes' => [ + 'id' => ['date-property'], + ], + '#value' => isset($fields['federated_date']) ? 'true' : variable_get('search_api_federated_solr_has_federated_date_property', 'true'), + ]; + + $form['search_form_values']['search_api_federated_solr_has_federated_type_property'] = [ + '#type' => 'hidden', + '#attributes' => [ + 'id' => ['type-property'], + ], + '#value' => isset($fields['federated_type']) ? 'true' : variable_get('search_api_federated_solr_has_federated_type_property', 'true'), + ]; + + $form['search_form_values']['search_api_federated_solr_has_federated_terms_property'] = [ + '#type' => 'hidden', + '#attributes' => [ + 'id' => ['terms-property'], + ], + '#value' => isset($fields['federated_terms']) ? 'true' : variable_get('search_api_federated_solr_has_federated_terms_property', 'true'), + ]; + + /** + * Enable hiding available facets / filters. + * These form elements will only be visible if their corresopnding + * property exists on the index. + */ + $form['search_form_values']['hidden'] = [ + '#type' => 'fieldset', + '#title' => t('Hide facets / filters from sidebar'), + '#description' => t('The checked facets / filters will be hidden from the search app.'), + ]; + + $form['search_form_values']['hidden']['search_api_federated_solr_hide_site_name'] = [ + '#type' => 'checkbox', + '#title' => t('Site name facet'), + '#default_value' => variable_get('search_api_federated_solr_hide_site_name'), + '#description' => t('When checked, the ability to select which sites should be included in the results will be hidden.'), + '#states' => [ + 'visible' => [ + ':input[name="site_name_property"]' => [ + 'value' => "true", + ], + ], + ], + ]; + + $form['search_form_values']['hidden']['search_api_federated_solr_hide_type'] = [ + '#type' => 'checkbox', + '#title' => t('Type facet'), + '#default_value' => variable_get('search_api_federated_solr_hide_type'), + '#description' => t('When checked, the ability to select those types (i.e. bundles) which should have results returned will be hidden.'), + '#states' => [ + 'visible' => [ + ':input[name="type_property"]' => [ + 'value' => "true", + ], + ], + ], + ]; + + $form['search_form_values']['hidden']['search_api_federated_solr_hide_date'] = [ + '#type' => 'checkbox', + '#title' => t('Date filter'), + '#default_value' => variable_get('search_api_federated_solr_hide_date'), + '#description' => t('When checked, the ability to filter results by date will be hidden.'), + '#states' => [ + 'visible' => [ + ':input[name="date_property"]' => [ + 'value' => "true", + ], + ], + ], + ]; + + $form['search_form_values']['hidden']['search_api_federated_solr_hide_terms'] = [ + '#type' => 'checkbox', + '#title' => t('Terms facet'), + '#default_value' => variable_get('search_api_federated_solr_hide_terms'), + '#description' => t('When checked, the ability to select those terms which should have results returned will be hidden.'), + '#states' => [ + 'visible' => [ + ':input[name="terms_property"]' => [ + 'value' => "true", + ], + ], + ], + ]; + + /** + * Autocomplete settings: + * - endpoint URL + * - use wildcard to support partial terms + * - customize number of autocomplete results + * - number of characters after which autocomplete query should be executed + * - autocomplete results mode (search results, terms) + * - title for autocomplete results + * - show/hide autocomplete keyboard directions + */ + + $form['autocomplete'] = [ + '#type' => 'fieldset', + '#title' => t('Search Results Page > Search Form > Autocomplete'), + '#description' => '

' . t('These options apply to the autocomplete functionality on the search for which appears above the search results on the search results page. Configure your placed Federated Search Page Form block to add autocomplete to that form.') . '

', + '#collapsible' => TRUE, + '#collapsed' => !variable_get('search_api_federated_solr_autocomplete_is_enabled'), + ]; + + $form['autocomplete']['search_api_federated_solr_autocomplete_is_enabled'] = [ + '#type' => 'checkbox', + '#title' => '' . t('Enable autocomplete for the search results page search form') . '', + '#default_value' => variable_get('search_api_federated_solr_autocomplete_is_enabled'), + '#description' => t('Checking this will expose more configuration options for autocomplete behavior for the search form on the Search Results page at the end of this form.'), + '#attributes' => [ + 'id' => ['autocomplete-is-enabled'], + ], + ]; + + $form['autocomplete']['search_api_federated_solr_autocomplete_is_append_wildcard'] = [ + '#type' => 'checkbox', + '#title' => '' . t('Append a wildcard \'*\' to support partial text search') . '', + '#default_value' => variable_get('search_api_federated_solr_autocomplete_is_append_wildcard'), + '#description' => t('Check this box to append a wildcard * to the end of the autocomplete query term (i.e. "car" becomes "car+car*"). This option is recommended if your solr config does not add a field(s) with NGram Tokenizers to your index or if your autocomplete Request Handler is not configured to search those fields.'), + '#states' => [ + 'visible' => [ + ':input[data-autocomplete-enabler]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + $form['autocomplete']['search_api_federated_solr_autocomplete_disable_query_proxy'] = [ + '#type' => 'checkbox', + '#title' => '' . t('Do not use the proxy for the search app autocomplete query') . '', + '#default_value' => variable_get('search_api_federated_solr_autocomplete_disable_query_proxy', 0), + '#description' => t('Check this box to configure the search app to query the Solr server directly. When checked, it is highly recommended that you also procure and configure read-only basic auth credentials for the search app. When unchecked, this site will act as a proxy for requests to the Solr server of the Search API index chosen above in Search Results Page > Set Up using the Drupal route defined by this module.

Note: Acquia Search customers must leave this box unchecked.'), + '#attributes' => [ + 'data-autocomplete-direct-query-enabler' => TRUE, + ], + '#states' => [ + 'visible' => [ + ':input[data-autocomplete-enabler]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + $form['autocomplete']['direct'] = [ + '#type' => 'fieldset', + '#title' => t('Autocomplete Direct Query Settings'), + '#states' => [ + 'visible' => [ + ':input[data-autocomplete-direct-query-enabler]' => [ + 'checked' => TRUE, + ], + ], + ], + ]; + + $form['autocomplete']['direct']['search_api_federated_solr_autocomplete_url'] = [ + '#type' => 'textfield', + '#title' => t('Solr Endpoint URL'), + '#default_value' => variable_get('search_api_federated_solr_autocomplete_url'), + '#maxlength' => 2048, + '#size' => 50, + '#description' => t('The URL where requests for autocomplete queries should be made. (Default: the url of the select Request Handler on the server of the selected Search API index.)