123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297 |
- import Hogan from "hogan.js";
- import LunrSearchAdapter from "./lunar-search";
- import autocomplete from "autocomplete.js";
- import templates from "./templates";
- import utils from "./utils";
- import $ from "autocomplete.js/zepto";
- class DocSearch {
- constructor({
- searchDocs,
- searchIndex,
- inputSelector,
- debug = false,
- baseUrl = '/',
- queryDataCallback = null,
- autocompleteOptions = {
- debug: false,
- hint: false,
- autoselect: true
- },
- transformData = false,
- queryHook = false,
- handleSelected = false,
- enhancedSearchInput = false,
- layout = "collumns"
- }) {
- this.input = DocSearch.getInputFromSelector(inputSelector);
- this.queryDataCallback = queryDataCallback || null;
- const autocompleteOptionsDebug =
- autocompleteOptions && autocompleteOptions.debug
- ? autocompleteOptions.debug
- : false;
- // eslint-disable-next-line no-param-reassign
- autocompleteOptions.debug = debug || autocompleteOptionsDebug;
- this.autocompleteOptions = autocompleteOptions;
- this.autocompleteOptions.cssClasses =
- this.autocompleteOptions.cssClasses || {};
- this.autocompleteOptions.cssClasses.prefix =
- this.autocompleteOptions.cssClasses.prefix || "ds";
- const inputAriaLabel =
- this.input &&
- typeof this.input.attr === "function" &&
- this.input.attr("aria-label");
- this.autocompleteOptions.ariaLabel =
- this.autocompleteOptions.ariaLabel || inputAriaLabel || "search input";
- this.isSimpleLayout = layout === "simple";
- this.client = new LunrSearchAdapter(searchDocs, searchIndex, baseUrl);
- if (enhancedSearchInput) {
- this.input = DocSearch.injectSearchBox(this.input);
- }
- this.autocomplete = autocomplete(this.input, autocompleteOptions, [
- {
- source: this.getAutocompleteSource(transformData, queryHook),
- templates: {
- suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
- footer: templates.footer,
- empty: DocSearch.getEmptyTemplate()
- }
- }
- ]);
- const customHandleSelected = handleSelected;
- this.handleSelected = customHandleSelected || this.handleSelected;
- // We prevent default link clicking if a custom handleSelected is defined
- if (customHandleSelected) {
- $(".algolia-autocomplete").on("click", ".ds-suggestions a", event => {
- event.preventDefault();
- });
- }
- this.autocomplete.on(
- "autocomplete:selected",
- this.handleSelected.bind(null, this.autocomplete.autocomplete)
- );
- this.autocomplete.on(
- "autocomplete:shown",
- this.handleShown.bind(null, this.input)
- );
- if (enhancedSearchInput) {
- DocSearch.bindSearchBoxEvent();
- }
- }
- static injectSearchBox(input) {
- input.before(templates.searchBox);
- const newInput = input
- .prev()
- .prev()
- .find("input");
- input.remove();
- return newInput;
- }
- static bindSearchBoxEvent() {
- $('.searchbox [type="reset"]').on("click", function () {
- $("input#docsearch").focus();
- $(this).addClass("hide");
- autocomplete.autocomplete.setVal("");
- });
- $("input#docsearch").on("keyup", () => {
- const searchbox = document.querySelector("input#docsearch");
- const reset = document.querySelector('.searchbox [type="reset"]');
- reset.className = "searchbox__reset";
- if (searchbox.value.length === 0) {
- reset.className += " hide";
- }
- });
- }
- /**
- * Returns the matching input from a CSS selector, null if none matches
- * @function getInputFromSelector
- * @param {string} selector CSS selector that matches the search
- * input of the page
- * @returns {void}
- */
- static getInputFromSelector(selector) {
- const input = $(selector).filter("input");
- return input.length ? $(input[0]) : null;
- }
- /**
- * Returns the `source` method to be passed to autocomplete.js. It will query
- * the Algolia index and call the callbacks with the formatted hits.
- * @function getAutocompleteSource
- * @param {function} transformData An optional function to transform the hits
- * @param {function} queryHook An optional function to transform the query
- * @returns {function} Method to be passed as the `source` option of
- * autocomplete
- */
- getAutocompleteSource(transformData, queryHook) {
- return (query, callback) => {
- if (queryHook) {
- // eslint-disable-next-line no-param-reassign
- query = queryHook(query) || query;
- }
- this.client.search(query).then(hits => {
- if (
- this.queryDataCallback &&
- typeof this.queryDataCallback == "function"
- ) {
- this.queryDataCallback(hits);
- }
- if (transformData) {
- hits = transformData(hits) || hits;
- }
- callback(DocSearch.formatHits(hits));
- });
- };
- }
- // Given a list of hits returned by the API, will reformat them to be used in
- // a Hogan template
- static formatHits(receivedHits) {
- const clonedHits = utils.deepClone(receivedHits);
- const hits = clonedHits.map(hit => {
- if (hit._highlightResult) {
- // eslint-disable-next-line no-param-reassign
- hit._highlightResult = utils.mergeKeyWithParent(
- hit._highlightResult,
- "hierarchy"
- );
- }
- return utils.mergeKeyWithParent(hit, "hierarchy");
- });
- // Group hits by category / subcategory
- let groupedHits = utils.groupBy(hits, "lvl0");
- $.each(groupedHits, (level, collection) => {
- const groupedHitsByLvl1 = utils.groupBy(collection, "lvl1");
- const flattenedHits = utils.flattenAndFlagFirst(
- groupedHitsByLvl1,
- "isSubCategoryHeader"
- );
- groupedHits[level] = flattenedHits;
- });
- groupedHits = utils.flattenAndFlagFirst(groupedHits, "isCategoryHeader");
- // Translate hits into smaller objects to be send to the template
- return groupedHits.map(hit => {
- const url = DocSearch.formatURL(hit);
- const category = utils.getHighlightedValue(hit, "lvl0");
- const subcategory = utils.getHighlightedValue(hit, "lvl1") || category;
- const displayTitle = utils
- .compact([
- utils.getHighlightedValue(hit, "lvl2") || subcategory,
- utils.getHighlightedValue(hit, "lvl3"),
- utils.getHighlightedValue(hit, "lvl4"),
- utils.getHighlightedValue(hit, "lvl5"),
- utils.getHighlightedValue(hit, "lvl6")
- ])
- .join(
- '<span class="aa-suggestion-title-separator" aria-hidden="true"> › </span>'
- );
- const text = utils.getSnippetedValue(hit, "content");
- const isTextOrSubcategoryNonEmpty =
- (subcategory && subcategory !== "") ||
- (displayTitle && displayTitle !== "");
- const isLvl1EmptyOrDuplicate =
- !subcategory || subcategory === "" || subcategory === category;
- const isLvl2 =
- displayTitle && displayTitle !== "" && displayTitle !== subcategory;
- const isLvl1 =
- !isLvl2 &&
- (subcategory && subcategory !== "" && subcategory !== category);
- const isLvl0 = !isLvl1 && !isLvl2;
- return {
- isLvl0,
- isLvl1,
- isLvl2,
- isLvl1EmptyOrDuplicate,
- isCategoryHeader: hit.isCategoryHeader,
- isSubCategoryHeader: hit.isSubCategoryHeader,
- isTextOrSubcategoryNonEmpty,
- category,
- subcategory,
- title: displayTitle,
- text,
- url
- };
- });
- }
- static formatURL(hit) {
- const { url, anchor } = hit;
- if (url) {
- const containsAnchor = url.indexOf("#") !== -1;
- if (containsAnchor) return url;
- else if (anchor) return `${hit.url}#${hit.anchor}`;
- return url;
- } else if (anchor) return `#${hit.anchor}`;
- /* eslint-disable */
- console.warn("no anchor nor url for : ", JSON.stringify(hit));
- /* eslint-enable */
- return null;
- }
- static getEmptyTemplate() {
- return args => Hogan.compile(templates.empty).render(args);
- }
- static getSuggestionTemplate(isSimpleLayout) {
- const stringTemplate = isSimpleLayout
- ? templates.suggestionSimple
- : templates.suggestion;
- const template = Hogan.compile(stringTemplate);
- return suggestion => template.render(suggestion);
- }
- handleSelected(input, event, suggestion, datasetNumber, context = {}) {
- // Do nothing if click on the suggestion, as it's already a <a href>, the
- // browser will take care of it. This allow Ctrl-Clicking on results and not
- // having the main window being redirected as well
- if (context.selectionMethod === "click") {
- return;
- }
- input.setVal("");
- window.location.assign(suggestion.url);
- }
- handleShown(input) {
- const middleOfInput = input.offset().left + input.width() / 2;
- let middleOfWindow = $(document).width() / 2;
- if (isNaN(middleOfWindow)) {
- middleOfWindow = 900;
- }
- const alignClass =
- middleOfInput - middleOfWindow >= 0
- ? "algolia-autocomplete-right"
- : "algolia-autocomplete-left";
- const otherAlignClass =
- middleOfInput - middleOfWindow < 0
- ? "algolia-autocomplete-right"
- : "algolia-autocomplete-left";
- const autocompleteWrapper = $(".algolia-autocomplete");
- if (!autocompleteWrapper.hasClass(alignClass)) {
- autocompleteWrapper.addClass(alignClass);
- }
- if (autocompleteWrapper.hasClass(otherAlignClass)) {
- autocompleteWrapper.removeClass(otherAlignClass);
- }
- }
- }
- export default DocSearch;
|