Browse Source

swizzle lunr searchbar

windhamdavid 3 years ago
parent
commit
6e35b1bbe2

File diff suppressed because it is too large
+ 0 - 0
src/theme/SearchBar/algolia.css


+ 118 - 106
src/theme/SearchBar/index.js

@@ -1,106 +1,118 @@
-/**
- * Copyright (c) 2017-present, Facebook, Inc.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
-
-import React, { useRef, useCallback } from "react";
-import classnames from "classnames";
-import { useHistory } from "@docusaurus/router";
-import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
-const Search = props => {
-  const initialized = useRef(false);
-  const searchBarRef = useRef(null);
-  const history = useHistory();
-  const { siteConfig = {} } = useDocusaurusContext();
-  const { baseUrl } = siteConfig;
-  const initAlgolia = (searchDocs, searchIndex, DocSearch) => {
-      new DocSearch({
-        searchDocs,
-        searchIndex,
-        inputSelector: "#search_input_react",
-        // Override algolia's default selection event, allowing us to do client-side
-        // navigation and avoiding a full page refresh.
-        handleSelected: (_input, _event, suggestion) => {
-          const url = baseUrl + suggestion.url;
-          // Use an anchor tag to parse the absolute url into a relative url
-          // Alternatively, we can use new URL(suggestion.url) but its not supported in IE
-          const a = document.createElement("a");
-          a.href = url;
-          // Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
-          // So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
-
-          history.push(url);
-        }
-      });
-  };
-
-  const getSearchDoc = () =>
-    process.env.NODE_ENV === "production"
-      ? fetch(`${baseUrl}search-doc.json`).then((content) => content.json())
-      : Promise.resolve([]);
-
-  const getLunrIndex = () =>
-    process.env.NODE_ENV === "production"
-      ? fetch(`${baseUrl}lunr-index.json`).then((content) => content.json())
-      : Promise.resolve([]);
-
-  const loadAlgolia = () => {
-    if (!initialized.current) {
-      Promise.all([
-        getSearchDoc(),
-        getLunrIndex(),
-        import("./lib/DocSearch"),
-        import("./algolia.css")
-      ]).then(([searchDocs, searchIndex, { default: DocSearch }]) => {
-        initAlgolia(searchDocs, searchIndex, DocSearch);
-      });
-      initialized.current = true;
-    }
-  };
-
-  const toggleSearchIconClick = useCallback(
-    e => {
-      if (!searchBarRef.current.contains(e.target)) {
-        searchBarRef.current.focus();
-      }
-
-      props.handleSearchBarToggle(!props.isSearchBarExpanded);
-    },
-    [props.isSearchBarExpanded]
-  );
-
-  return (
-    <div className="navbar__search" key="search-box">
-      <span
-        aria-label="expand searchbar"
-        role="button"
-        className={classnames("search-icon", {
-          "search-icon-hidden": props.isSearchBarExpanded
-        })}
-        onClick={toggleSearchIconClick}
-        onKeyDown={toggleSearchIconClick}
-        tabIndex={0}
-      />
-      <input
-        id="search_input_react"
-        type="search"
-        placeholder="Search"
-        aria-label="Search"
-        className={classnames(
-          "navbar__search-input",
-          { "search-bar-expanded": props.isSearchBarExpanded },
-          { "search-bar": !props.isSearchBarExpanded }
-        )}
-        onClick={loadAlgolia}
-        onMouseOver={loadAlgolia}
-        onFocus={toggleSearchIconClick}
-        onBlur={toggleSearchIconClick}
-        ref={searchBarRef}
-      />
-    </div>
-  );
-};
-
-export default Search;
+/**
+ * Copyright (c) 2017-present, Facebook, Inc.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import React, { useRef, useCallback, useState } from "react";
+import classnames from "classnames";
+import { useHistory } from "@docusaurus/router";
+import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
+import { usePluginData } from '@docusaurus/useGlobalData';
+const Search = props => {
+  const initialized = useRef(false);
+  const searchBarRef = useRef(null);
+  const [indexReady, setIndexReady] = useState(false);
+  const history = useHistory();
+  const { siteConfig = {}, isClient = false} = useDocusaurusContext();
+  const { baseUrl } = siteConfig;
+  const initAlgolia = (searchDocs, searchIndex, DocSearch) => {
+      new DocSearch({
+        searchDocs,
+        searchIndex,
+        inputSelector: "#search_input_react",
+        // Override algolia's default selection event, allowing us to do client-side
+        // navigation and avoiding a full page refresh.
+        handleSelected: (_input, _event, suggestion) => {
+          const url = baseUrl + suggestion.url;
+          // Use an anchor tag to parse the absolute url into a relative url
+          // Alternatively, we can use new URL(suggestion.url) but its not supported in IE
+          const a = document.createElement("a");
+          a.href = url;
+          // Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
+          // So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
+
+          history.push(url);
+        }
+      });
+  };
+
+  const pluginData = usePluginData('docusaurus-lunr-search');
+  const getSearchDoc = () =>
+    process.env.NODE_ENV === "production"
+      ? fetch(`${baseUrl}${pluginData.fileNames.searchDoc}`).then((content) => content.json())
+      : Promise.resolve([]);
+
+  const getLunrIndex = () =>
+    process.env.NODE_ENV === "production"
+      ? fetch(`${baseUrl}${pluginData.fileNames.lunrIndex}`).then((content) => content.json())
+      : Promise.resolve([]);
+
+  const loadAlgolia = () => {
+    if (!initialized.current) {
+      Promise.all([
+        getSearchDoc(),
+        getLunrIndex(),
+        import("./lib/DocSearch"),
+        import("./algolia.css")
+      ]).then(([searchDocs, searchIndex, { default: DocSearch }]) => {
+        if( searchDocs.length === 0) {
+          return;
+        }
+        initAlgolia(searchDocs, searchIndex, DocSearch);
+        setIndexReady(true);
+      });
+      initialized.current = true;
+    }
+  };
+
+  const toggleSearchIconClick = useCallback(
+    e => {
+      if (!searchBarRef.current.contains(e.target)) {
+        searchBarRef.current.focus();
+      }
+
+      props.handleSearchBarToggle && props.handleSearchBarToggle(!props.isSearchBarExpanded);
+    },
+    [props.isSearchBarExpanded]
+  );
+
+  if (isClient) {
+    loadAlgolia();
+  }
+
+  return (
+    <div className="navbar__search" key="search-box">
+      <span
+        aria-label="expand searchbar"
+        role="button"
+        className={classnames("search-icon", {
+          "search-icon-hidden": props.isSearchBarExpanded
+        })}
+        onClick={toggleSearchIconClick}
+        onKeyDown={toggleSearchIconClick}
+        tabIndex={0}
+      />
+      <input
+        id="search_input_react"
+        type="search"
+        placeholder={indexReady ? 'Search' : 'Loading...'}
+        aria-label="Search"
+        className={classnames(
+          "navbar__search-input",
+          { "search-bar-expanded": props.isSearchBarExpanded },
+          { "search-bar": !props.isSearchBarExpanded }
+        )}
+        onClick={loadAlgolia}
+        onMouseOver={loadAlgolia}
+        onFocus={toggleSearchIconClick}
+        onBlur={toggleSearchIconClick}
+        ref={searchBarRef}
+        disabled={!indexReady}
+      />
+    </div>
+  );
+};
+
+export default Search;

+ 305 - 305
src/theme/SearchBar/lib/DocSearch.js

@@ -1,306 +1,306 @@
-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";
-
-/**
- * Adds an autocomplete dropdown to an input field
- * @function DocSearch
- * @param  {Object} options.searchDocs Search Documents
- * @param  {Object} options.searchIndex Lune searchIndexes
- * @param  {string} options.inputSelector  CSS selector that targets the input
- * value.
- * @param  {Object} [options.autocompleteOptions] Options to pass to the underlying autocomplete instance
- * @return {Object}
- */
-class DocSearch {
-    constructor({
-        searchDocs,
-        searchIndex,
-        inputSelector,
-        debug = false,
-        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);
-
-        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);
-        }
-    }
-}
-
+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";
+
+/**
+ * Adds an autocomplete dropdown to an input field
+ * @function DocSearch
+ * @param  {Object} options.searchDocs Search Documents
+ * @param  {Object} options.searchIndex Lune searchIndexes
+ * @param  {string} options.inputSelector  CSS selector that targets the input
+ * value.
+ * @param  {Object} [options.autocompleteOptions] Options to pass to the underlying autocomplete instance
+ * @return {Object}
+ */
+class DocSearch {
+    constructor({
+        searchDocs,
+        searchIndex,
+        inputSelector,
+        debug = false,
+        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);
+
+        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;

+ 146 - 146
src/theme/SearchBar/lib/lunar-search.js

@@ -1,146 +1,146 @@
-import lunr from "@generated/lunr.client";
-lunr.tokenizer.separator = /[\s\-/]+/;
-
-class LunrSearchAdapter {
-    constructor(searchDocs, searchIndex) {
-        this.searchDocs = searchDocs;
-        this.lunrIndex = lunr.Index.load(searchIndex);
-    }
-
-    getLunrResult(input) {
-        return this.lunrIndex.query(function (query) {
-            const tokens = lunr.tokenizer(input);
-            query.term(tokens, {
-                boost: 10
-            });
-            query.term(tokens, {
-                wildcard: lunr.Query.wildcard.TRAILING
-            });
-        });
-    }
-
-    getHit(doc, formattedTitle, formattedContent) {
-        return {
-            hierarchy: {
-                lvl0: doc.pageTitle || doc.title,
-                lvl1: doc.type === 0 ? null : doc.title
-            },
-            url: doc.url,
-            _snippetResult: formattedContent ? {
-                content: {
-                    value: formattedContent,
-                    matchLevel: "full"
-                }
-            } : null,
-            _highlightResult: {
-                hierarchy: {
-                    lvl0: {
-                        value: doc.type === 0 ? formattedTitle || doc.title : doc.pageTitle,
-                    },
-                    lvl1:
-                        doc.type === 0
-                            ? null
-                            : {
-                                value: formattedTitle || doc.title
-                            }
-                }
-            }
-        };
-    }
-    getTitleHit(doc, position, length) {
-        const start = position[0];
-        const end = position[0] + length;
-        let formattedTitle = doc.title.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.title.substring(start, end) + '</span>' + doc.title.substring(end, doc.title.length);
-        return this.getHit(doc, formattedTitle)
-    }
-
-    getKeywordHit(doc, position, length) {
-        const start = position[0];
-        const end = position[0] + length;
-        let formattedTitle = doc.title + '<br /><i>Keywords: ' + doc.keywords.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.keywords.substring(start, end) + '</span>' + doc.keywords.substring(end, doc.keywords.length) + '</i>'
-        return this.getHit(doc, formattedTitle)
-    }
-
-    getContentHit(doc, position) {
-        const start = position[0];
-        const end = position[0] + position[1];
-        let previewStart = start;
-        let previewEnd = end;
-        let ellipsesBefore = true;
-        let ellipsesAfter = true;
-        for (let k = 0; k < 3; k++) {
-            const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
-            const nextDot = doc.content.lastIndexOf('.', previewStart - 2);
-            if ((nextDot > 0) && (nextDot > nextSpace)) {
-                previewStart = nextDot + 1;
-                ellipsesBefore = false;
-                break;
-            }
-            if (nextSpace < 0) {
-                previewStart = 0;
-                ellipsesBefore = false;
-                break;
-            }
-            previewStart = nextSpace + 1;
-        }
-        for (let k = 0; k < 10; k++) {
-            const nextSpace = doc.content.indexOf(' ', previewEnd + 1);
-            const nextDot = doc.content.indexOf('.', previewEnd + 1);
-            if ((nextDot > 0) && (nextDot < nextSpace)) {
-                previewEnd = nextDot;
-                ellipsesAfter = false;
-                break;
-            }
-            if (nextSpace < 0) {
-                previewEnd = doc.content.length;
-                ellipsesAfter = false;
-                break;
-            }
-            previewEnd = nextSpace;
-        }
-        let preview = doc.content.substring(previewStart, start);
-        if (ellipsesBefore) {
-            preview = '... ' + preview;
-        }
-        preview += '<span class="algolia-docsearch-suggestion--highlight">' + doc.content.substring(start, end) + '</span>';
-        preview += doc.content.substring(end, previewEnd);
-        if (ellipsesAfter) {
-            preview += ' ...';
-        }
-        return this.getHit(doc, null, preview);
-
-    }
-    search(input) {
-        return new Promise((resolve, rej) => {
-            const results = this.getLunrResult(input);
-            const hits = [];
-            results.length > 5 && (results.length = 5);
-            this.titleHitsRes = []
-            this.contentHitsRes = []
-            results.forEach(result => {
-                const doc = this.searchDocs[result.ref];
-                const { metadata } = result.matchData;
-                for (let i in metadata) {
-                    if (metadata[i].title) {
-                        if (!this.titleHitsRes.includes(result.ref)) {
-                            const position = metadata[i].title.position[0]
-                            hits.push(this.getTitleHit(doc, position, input.length));
-                            this.titleHitsRes.push(result.ref);
-                        }
-                    } else if (metadata[i].content) {
-                        const position = metadata[i].content.position[0]
-                        hits.push(this.getContentHit(doc, position))
-                    } else if (metadata[i].keywords) {
-                        const position = metadata[i].keywords.position[0]
-                        hits.push(this.getKeywordHit(doc, position, input.length));
-                        this.titleHitsRes.push(result.ref);
-                    }
-                }
-            });
-            hits.length > 5 && (hits.length = 5);
-            resolve(hits);
-        });
-    }
-}
-
-export default LunrSearchAdapter;
+import lunr from "@generated/lunr.client";
+lunr.tokenizer.separator = /[\s\-/]+/;
+
+class LunrSearchAdapter {
+    constructor(searchDocs, searchIndex) {
+        this.searchDocs = searchDocs;
+        this.lunrIndex = lunr.Index.load(searchIndex);
+    }
+
+    getLunrResult(input) {
+        return this.lunrIndex.query(function (query) {
+            const tokens = lunr.tokenizer(input);
+            query.term(tokens, {
+                boost: 10
+            });
+            query.term(tokens, {
+                wildcard: lunr.Query.wildcard.TRAILING
+            });
+        });
+    }
+
+    getHit(doc, formattedTitle, formattedContent) {
+        return {
+            hierarchy: {
+                lvl0: doc.pageTitle || doc.title,
+                lvl1: doc.type === 0 ? null : doc.title
+            },
+            url: doc.url,
+            _snippetResult: formattedContent ? {
+                content: {
+                    value: formattedContent,
+                    matchLevel: "full"
+                }
+            } : null,
+            _highlightResult: {
+                hierarchy: {
+                    lvl0: {
+                        value: doc.type === 0 ? formattedTitle || doc.title : doc.pageTitle,
+                    },
+                    lvl1:
+                        doc.type === 0
+                            ? null
+                            : {
+                                value: formattedTitle || doc.title
+                            }
+                }
+            }
+        };
+    }
+    getTitleHit(doc, position, length) {
+        const start = position[0];
+        const end = position[0] + length;
+        let formattedTitle = doc.title.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.title.substring(start, end) + '</span>' + doc.title.substring(end, doc.title.length);
+        return this.getHit(doc, formattedTitle)
+    }
+
+    getKeywordHit(doc, position, length) {
+        const start = position[0];
+        const end = position[0] + length;
+        let formattedTitle = doc.title + '<br /><i>Keywords: ' + doc.keywords.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.keywords.substring(start, end) + '</span>' + doc.keywords.substring(end, doc.keywords.length) + '</i>'
+        return this.getHit(doc, formattedTitle)
+    }
+
+    getContentHit(doc, position) {
+        const start = position[0];
+        const end = position[0] + position[1];
+        let previewStart = start;
+        let previewEnd = end;
+        let ellipsesBefore = true;
+        let ellipsesAfter = true;
+        for (let k = 0; k < 3; k++) {
+            const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
+            const nextDot = doc.content.lastIndexOf('.', previewStart - 2);
+            if ((nextDot > 0) && (nextDot > nextSpace)) {
+                previewStart = nextDot + 1;
+                ellipsesBefore = false;
+                break;
+            }
+            if (nextSpace < 0) {
+                previewStart = 0;
+                ellipsesBefore = false;
+                break;
+            }
+            previewStart = nextSpace + 1;
+        }
+        for (let k = 0; k < 10; k++) {
+            const nextSpace = doc.content.indexOf(' ', previewEnd + 1);
+            const nextDot = doc.content.indexOf('.', previewEnd + 1);
+            if ((nextDot > 0) && (nextDot < nextSpace)) {
+                previewEnd = nextDot;
+                ellipsesAfter = false;
+                break;
+            }
+            if (nextSpace < 0) {
+                previewEnd = doc.content.length;
+                ellipsesAfter = false;
+                break;
+            }
+            previewEnd = nextSpace;
+        }
+        let preview = doc.content.substring(previewStart, start);
+        if (ellipsesBefore) {
+            preview = '... ' + preview;
+        }
+        preview += '<span class="algolia-docsearch-suggestion--highlight">' + doc.content.substring(start, end) + '</span>';
+        preview += doc.content.substring(end, previewEnd);
+        if (ellipsesAfter) {
+            preview += ' ...';
+        }
+        return this.getHit(doc, null, preview);
+
+    }
+    search(input) {
+        return new Promise((resolve, rej) => {
+            const results = this.getLunrResult(input);
+            const hits = [];
+            results.length > 5 && (results.length = 5);
+            this.titleHitsRes = []
+            this.contentHitsRes = []
+            results.forEach(result => {
+                const doc = this.searchDocs[result.ref];
+                const { metadata } = result.matchData;
+                for (let i in metadata) {
+                    if (metadata[i].title) {
+                        if (!this.titleHitsRes.includes(result.ref)) {
+                            const position = metadata[i].title.position[0]
+                            hits.push(this.getTitleHit(doc, position, input.length));
+                            this.titleHitsRes.push(result.ref);
+                        }
+                    } else if (metadata[i].content) {
+                        const position = metadata[i].content.position[0]
+                        hits.push(this.getContentHit(doc, position))
+                    } else if (metadata[i].keywords) {
+                        const position = metadata[i].keywords.position[0]
+                        hits.push(this.getKeywordHit(doc, position, input.length));
+                        this.titleHitsRes.push(result.ref);
+                    }
+                }
+            });
+            hits.length > 5 && (hits.length = 5);
+            resolve(hits);
+        });
+    }
+}
+
+export default LunrSearchAdapter;

+ 114 - 114
src/theme/SearchBar/lib/templates.js

@@ -1,114 +1,114 @@
-const prefix = 'algolia-docsearch';
-const suggestionPrefix = `${prefix}-suggestion`;
-const footerPrefix = `${prefix}-footer`;
-
-/* eslint-disable max-len */
-
-const templates = {
-  suggestion: `
-  <a class="${suggestionPrefix}
-    {{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
-    {{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
-    "
-    aria-label="Link to the result"
-    href="{{{url}}}"
-    >
-    <div class="${suggestionPrefix}--category-header">
-        <span class="${suggestionPrefix}--category-header-lvl0">{{{category}}}</span>
-    </div>
-    <div class="${suggestionPrefix}--wrapper">
-      <div class="${suggestionPrefix}--subcategory-column">
-        <span class="${suggestionPrefix}--subcategory-column-text">{{{subcategory}}}</span>
-      </div>
-      {{#isTextOrSubcategoryNonEmpty}}
-      <div class="${suggestionPrefix}--content">
-        <div class="${suggestionPrefix}--subcategory-inline">{{{subcategory}}}</div>
-        <div class="${suggestionPrefix}--title">{{{title}}}</div>
-        {{#text}}<div class="${suggestionPrefix}--text">{{{text}}}</div>{{/text}}
-      </div>
-      {{/isTextOrSubcategoryNonEmpty}}
-    </div>
-  </a>
-  `,
-  suggestionSimple: `
-  <div class="${suggestionPrefix}
-    {{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
-    {{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
-    suggestion-layout-simple
-  ">
-    <div class="${suggestionPrefix}--category-header">
-        {{^isLvl0}}
-        <span class="${suggestionPrefix}--category-header-lvl0 ${suggestionPrefix}--category-header-item">{{{category}}}</span>
-          {{^isLvl1}}
-          {{^isLvl1EmptyOrDuplicate}}
-          <span class="${suggestionPrefix}--category-header-lvl1 ${suggestionPrefix}--category-header-item">
-              {{{subcategory}}}
-          </span>
-          {{/isLvl1EmptyOrDuplicate}}
-          {{/isLvl1}}
-        {{/isLvl0}}
-        <div class="${suggestionPrefix}--title ${suggestionPrefix}--category-header-item">
-            {{#isLvl2}}
-                {{{title}}}
-            {{/isLvl2}}
-            {{#isLvl1}}
-                {{{subcategory}}}
-            {{/isLvl1}}
-            {{#isLvl0}}
-                {{{category}}}
-            {{/isLvl0}}
-        </div>
-    </div>
-    <div class="${suggestionPrefix}--wrapper">
-      {{#text}}
-      <div class="${suggestionPrefix}--content">
-        <div class="${suggestionPrefix}--text">{{{text}}}</div>
-      </div>
-      {{/text}}
-    </div>
-  </div>
-  `,
-  footer: `
-    <div class="${footerPrefix}">
-    </div>
-  `,
-  empty: `
-  <div class="${suggestionPrefix}">
-    <div class="${suggestionPrefix}--wrapper">
-        <div class="${suggestionPrefix}--content ${suggestionPrefix}--no-results">
-            <div class="${suggestionPrefix}--title">
-                <div class="${suggestionPrefix}--text">
-                    No results found for query <b>"{{query}}"</b>
-                </div>
-            </div>
-        </div>
-    </div>
-  </div>
-  `,
-  searchBox: `
-  <form novalidate="novalidate" onsubmit="return false;" class="searchbox">
-    <div role="search" class="searchbox__wrapper">
-      <input id="docsearch" type="search" name="search" placeholder="Search the docs" autocomplete="off" required="required" class="searchbox__input"/>
-      <button type="submit" title="Submit your search query." class="searchbox__submit" >
-        <svg width=12 height=12 role="img" aria-label="Search">
-          <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-search-13"></use>
-        </svg>
-      </button>
-      <button type="reset" title="Clear the search query." class="searchbox__reset hide">
-        <svg width=12 height=12 role="img" aria-label="Reset">
-          <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-clear-3"></use>
-        </svg>
-      </button>
-    </div>
-</form>
-
-<div class="svg-icons" style="height: 0; width: 0; position: absolute; visibility: hidden">
-  <svg xmlns="http://www.w3.org/2000/svg">
-    <symbol id="sbx-icon-clear-3" viewBox="0 0 40 40"><path d="M16.228 20L1.886 5.657 0 3.772 3.772 0l1.885 1.886L20 16.228 34.343 1.886 36.228 0 40 3.772l-1.886 1.885L23.772 20l14.342 14.343L40 36.228 36.228 40l-1.885-1.886L20 23.772 5.657 38.114 3.772 40 0 36.228l1.886-1.885L16.228 20z" fill-rule="evenodd"></symbol>
-    <symbol id="sbx-icon-search-13" viewBox="0 0 40 40"><path d="M26.806 29.012a16.312 16.312 0 0 1-10.427 3.746C7.332 32.758 0 25.425 0 16.378 0 7.334 7.333 0 16.38 0c9.045 0 16.378 7.333 16.378 16.38 0 3.96-1.406 7.593-3.746 10.426L39.547 37.34c.607.608.61 1.59-.004 2.203a1.56 1.56 0 0 1-2.202.004L26.807 29.012zm-10.427.627c7.322 0 13.26-5.938 13.26-13.26 0-7.324-5.938-13.26-13.26-13.26-7.324 0-13.26 5.936-13.26 13.26 0 7.322 5.936 13.26 13.26 13.26z" fill-rule="evenodd"></symbol>
-  </svg>
-</div>
-  `,
-};
-
-export default templates;
+const prefix = 'algolia-docsearch';
+const suggestionPrefix = `${prefix}-suggestion`;
+const footerPrefix = `${prefix}-footer`;
+
+/* eslint-disable max-len */
+
+const templates = {
+  suggestion: `
+  <a class="${suggestionPrefix}
+    {{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
+    {{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
+    "
+    aria-label="Link to the result"
+    href="{{{url}}}"
+    >
+    <div class="${suggestionPrefix}--category-header">
+        <span class="${suggestionPrefix}--category-header-lvl0">{{{category}}}</span>
+    </div>
+    <div class="${suggestionPrefix}--wrapper">
+      <div class="${suggestionPrefix}--subcategory-column">
+        <span class="${suggestionPrefix}--subcategory-column-text">{{{subcategory}}}</span>
+      </div>
+      {{#isTextOrSubcategoryNonEmpty}}
+      <div class="${suggestionPrefix}--content">
+        <div class="${suggestionPrefix}--subcategory-inline">{{{subcategory}}}</div>
+        <div class="${suggestionPrefix}--title">{{{title}}}</div>
+        {{#text}}<div class="${suggestionPrefix}--text">{{{text}}}</div>{{/text}}
+      </div>
+      {{/isTextOrSubcategoryNonEmpty}}
+    </div>
+  </a>
+  `,
+  suggestionSimple: `
+  <div class="${suggestionPrefix}
+    {{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
+    {{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
+    suggestion-layout-simple
+  ">
+    <div class="${suggestionPrefix}--category-header">
+        {{^isLvl0}}
+        <span class="${suggestionPrefix}--category-header-lvl0 ${suggestionPrefix}--category-header-item">{{{category}}}</span>
+          {{^isLvl1}}
+          {{^isLvl1EmptyOrDuplicate}}
+          <span class="${suggestionPrefix}--category-header-lvl1 ${suggestionPrefix}--category-header-item">
+              {{{subcategory}}}
+          </span>
+          {{/isLvl1EmptyOrDuplicate}}
+          {{/isLvl1}}
+        {{/isLvl0}}
+        <div class="${suggestionPrefix}--title ${suggestionPrefix}--category-header-item">
+            {{#isLvl2}}
+                {{{title}}}
+            {{/isLvl2}}
+            {{#isLvl1}}
+                {{{subcategory}}}
+            {{/isLvl1}}
+            {{#isLvl0}}
+                {{{category}}}
+            {{/isLvl0}}
+        </div>
+    </div>
+    <div class="${suggestionPrefix}--wrapper">
+      {{#text}}
+      <div class="${suggestionPrefix}--content">
+        <div class="${suggestionPrefix}--text">{{{text}}}</div>
+      </div>
+      {{/text}}
+    </div>
+  </div>
+  `,
+  footer: `
+    <div class="${footerPrefix}">
+    </div>
+  `,
+  empty: `
+  <div class="${suggestionPrefix}">
+    <div class="${suggestionPrefix}--wrapper">
+        <div class="${suggestionPrefix}--content ${suggestionPrefix}--no-results">
+            <div class="${suggestionPrefix}--title">
+                <div class="${suggestionPrefix}--text">
+                    No results found for query <b>"{{query}}"</b>
+                </div>
+            </div>
+        </div>
+    </div>
+  </div>
+  `,
+  searchBox: `
+  <form novalidate="novalidate" onsubmit="return false;" class="searchbox">
+    <div role="search" class="searchbox__wrapper">
+      <input id="docsearch" type="search" name="search" placeholder="Search the docs" autocomplete="off" required="required" class="searchbox__input"/>
+      <button type="submit" title="Submit your search query." class="searchbox__submit" >
+        <svg width=12 height=12 role="img" aria-label="Search">
+          <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-search-13"></use>
+        </svg>
+      </button>
+      <button type="reset" title="Clear the search query." class="searchbox__reset hide">
+        <svg width=12 height=12 role="img" aria-label="Reset">
+          <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-clear-3"></use>
+        </svg>
+      </button>
+    </div>
+</form>
+
+<div class="svg-icons" style="height: 0; width: 0; position: absolute; visibility: hidden">
+  <svg xmlns="http://www.w3.org/2000/svg">
+    <symbol id="sbx-icon-clear-3" viewBox="0 0 40 40"><path d="M16.228 20L1.886 5.657 0 3.772 3.772 0l1.885 1.886L20 16.228 34.343 1.886 36.228 0 40 3.772l-1.886 1.885L23.772 20l14.342 14.343L40 36.228 36.228 40l-1.885-1.886L20 23.772 5.657 38.114 3.772 40 0 36.228l1.886-1.885L16.228 20z" fill-rule="evenodd"></symbol>
+    <symbol id="sbx-icon-search-13" viewBox="0 0 40 40"><path d="M26.806 29.012a16.312 16.312 0 0 1-10.427 3.746C7.332 32.758 0 25.425 0 16.378 0 7.334 7.333 0 16.38 0c9.045 0 16.378 7.333 16.378 16.38 0 3.96-1.406 7.593-3.746 10.426L39.547 37.34c.607.608.61 1.59-.004 2.203a1.56 1.56 0 0 1-2.202.004L26.807 29.012zm-10.427.627c7.322 0 13.26-5.938 13.26-13.26 0-7.324-5.938-13.26-13.26-13.26-7.324 0-13.26 5.936-13.26 13.26 0 7.322 5.936 13.26 13.26 13.26z" fill-rule="evenodd"></symbol>
+  </svg>
+</div>
+  `,
+};
+
+export default templates;

+ 270 - 270
src/theme/SearchBar/lib/utils.js

@@ -1,270 +1,270 @@
-import $ from "autocomplete.js/zepto";
-
-const utils = {
-  /*
-  * Move the content of an object key one level higher.
-  * eg.
-  * {
-  *   name: 'My name',
-  *   hierarchy: {
-  *     lvl0: 'Foo',
-  *     lvl1: 'Bar'
-  *   }
-  * }
-  * Will be converted to
-  * {
-  *   name: 'My name',
-  *   lvl0: 'Foo',
-  *   lvl1: 'Bar'
-  * }
-  * @param {Object} object Main object
-  * @param {String} property Main object key to move up
-  * @return {Object}
-  * @throws Error when key is not an attribute of Object or is not an object itself
-  */
-  mergeKeyWithParent(object, property) {
-    if (object[property] === undefined) {
-      return object;
-    }
-    if (typeof object[property] !== 'object') {
-      return object;
-    }
-    const newObject = $.extend({}, object, object[property]);
-    delete newObject[property];
-    return newObject;
-  },
-  /*
-  * Group all objects of a collection by the value of the specified attribute
-  * If the attribute is a string, use the lowercase form.
-  *
-  * eg.
-  * groupBy([
-  *   {name: 'Tim', category: 'dev'},
-  *   {name: 'Vincent', category: 'dev'},
-  *   {name: 'Ben', category: 'sales'},
-  *   {name: 'Jeremy', category: 'sales'},
-  *   {name: 'AlexS', category: 'dev'},
-  *   {name: 'AlexK', category: 'sales'}
-  * ], 'category');
-  * =>
-  * {
-  *   'devs': [
-  *     {name: 'Tim', category: 'dev'},
-  *     {name: 'Vincent', category: 'dev'},
-  *     {name: 'AlexS', category: 'dev'}
-  *   ],
-  *   'sales': [
-  *     {name: 'Ben', category: 'sales'},
-  *     {name: 'Jeremy', category: 'sales'},
-  *     {name: 'AlexK', category: 'sales'}
-  *   ]
-  * }
-  * @param {array} collection Array of objects to group
-  * @param {String} property The attribute on which apply the grouping
-  * @return {array}
-  * @throws Error when one of the element does not have the specified property
-  */
-  groupBy(collection, property) {
-    const newCollection = {};
-    $.each(collection, (index, item) => {
-      if (item[property] === undefined) {
-        throw new Error(`[groupBy]: Object has no key ${property}`);
-      }
-      let key = item[property];
-      if (typeof key === 'string') {
-        key = key.toLowerCase();
-      }
-      // fix #171 the given data type of docsearch hits might be conflict with the properties of the native Object,
-      // such as the constructor, so we need to do this check.
-      if (!Object.prototype.hasOwnProperty.call(newCollection, key)) {
-        newCollection[key] = [];
-      }
-      newCollection[key].push(item);
-    });
-    return newCollection;
-  },
-  /*
-  * Return an array of all the values of the specified object
-  * eg.
-  * values({
-  *   foo: 42,
-  *   bar: true,
-  *   baz: 'yep'
-  * })
-  * =>
-  * [42, true, yep]
-  * @param {object} object Object to extract values from
-  * @return {array}
-  */
-  values(object) {
-    return Object.keys(object).map(key => object[key]);
-  },
-  /*
-  * Flattens an array
-  * eg.
-  * flatten([1, 2, [3, 4], [5, 6]])
-  * =>
-  * [1, 2, 3, 4, 5, 6]
-  * @param {array} array Array to flatten
-  * @return {array}
-  */
-  flatten(array) {
-    const results = [];
-    array.forEach(value => {
-      if (!Array.isArray(value)) {
-        results.push(value);
-        return;
-      }
-      value.forEach(subvalue => {
-        results.push(subvalue);
-      });
-    });
-    return results;
-  },
-  /*
-  * Flatten all values of an object into an array, marking each first element of
-  * each group with a specific flag
-  * eg.
-  * flattenAndFlagFirst({
-  *   'devs': [
-  *     {name: 'Tim', category: 'dev'},
-  *     {name: 'Vincent', category: 'dev'},
-  *     {name: 'AlexS', category: 'dev'}
-  *   ],
-  *   'sales': [
-  *     {name: 'Ben', category: 'sales'},
-  *     {name: 'Jeremy', category: 'sales'},
-  *     {name: 'AlexK', category: 'sales'}
-  *   ]
-  * , 'isTop');
-  * =>
-  * [
-  *     {name: 'Tim', category: 'dev', isTop: true},
-  *     {name: 'Vincent', category: 'dev', isTop: false},
-  *     {name: 'AlexS', category: 'dev', isTop: false},
-  *     {name: 'Ben', category: 'sales', isTop: true},
-  *     {name: 'Jeremy', category: 'sales', isTop: false},
-  *     {name: 'AlexK', category: 'sales', isTop: false}
-  * ]
-  * @param {object} object Object to flatten
-  * @param {string} flag Flag to set to true on first element of each group
-  * @return {array}
-  */
-  flattenAndFlagFirst(object, flag) {
-    const values = this.values(object).map(collection =>
-      collection.map((item, index) => {
-        // eslint-disable-next-line no-param-reassign
-        item[flag] = index === 0;
-        return item;
-      })
-    );
-    return this.flatten(values);
-  },
-  /*
-  * Removes all empty strings, null, false and undefined elements array
-  * eg.
-  * compact([42, false, null, undefined, '', [], 'foo']);
-  * =>
-  * [42, [], 'foo']
-  * @param {array} array Array to compact
-  * @return {array}
-  */
-  compact(array) {
-    const results = [];
-    array.forEach(value => {
-      if (!value) {
-        return;
-      }
-      results.push(value);
-    });
-    return results;
-  },
-  /*
-   * Returns the highlighted value of the specified key in the specified object.
-   * If no highlighted value is available, will return the key value directly
-   * eg.
-   * getHighlightedValue({
-   *    _highlightResult: {
-   *      text: {
-   *        value: '<mark>foo</mark>'
-   *      }
-   *    },
-   *    text: 'foo'
-   * }, 'text');
-   * =>
-   * '<mark>foo</mark>'
-   * @param {object} object Hit object returned by the Algolia API
-   * @param {string} property Object key to look for
-   * @return {string}
-   **/
-  getHighlightedValue(object, property) {
-    if (
-      object._highlightResult &&
-      object._highlightResult.hierarchy_camel &&
-      object._highlightResult.hierarchy_camel[property] &&
-      object._highlightResult.hierarchy_camel[property].matchLevel &&
-      object._highlightResult.hierarchy_camel[property].matchLevel !== 'none' &&
-      object._highlightResult.hierarchy_camel[property].value
-    ) {
-      return object._highlightResult.hierarchy_camel[property].value;
-    }
-    if (
-      object._highlightResult &&
-      object._highlightResult &&
-      object._highlightResult[property] &&
-      object._highlightResult[property].value
-    ) {
-      return object._highlightResult[property].value;
-    }
-    return object[property];
-  },
-  /*
-   * Returns the snippeted value of the specified key in the specified object.
-   * If no highlighted value is available, will return the key value directly.
-   * Will add starting and ending ellipsis (…) if we detect that a sentence is
-   * incomplete
-   * eg.
-   * getSnippetedValue({
-   *    _snippetResult: {
-   *      text: {
-   *        value: '<mark>This is an unfinished sentence</mark>'
-   *      }
-   *    },
-   *    text: 'This is an unfinished sentence'
-   * }, 'text');
-   * =>
-   * '<mark>This is an unfinished sentence</mark>…'
-   * @param {object} object Hit object returned by the Algolia API
-   * @param {string} property Object key to look for
-   * @return {string}
-   **/
-  getSnippetedValue(object, property) {
-    if (
-      !object._snippetResult ||
-      !object._snippetResult[property] ||
-      !object._snippetResult[property].value
-    ) {
-      return object[property];
-    }
-    let snippet = object._snippetResult[property].value;
-
-    if (snippet[0] !== snippet[0].toUpperCase()) {
-      snippet = `…${snippet}`;
-    }
-    if (['.', '!', '?'].indexOf(snippet[snippet.length - 1]) === -1) {
-      snippet = `${snippet}…`;
-    }
-    return snippet;
-  },
-  /*
-  * Deep clone an object.
-  * Note: This will not clone functions and dates
-  * @param {object} object Object to clone
-  * @return {object}
-  */
-  deepClone(object) {
-    return JSON.parse(JSON.stringify(object));
-  },
-};
-
-export default utils;
+import $ from "autocomplete.js/zepto";
+
+const utils = {
+  /*
+  * Move the content of an object key one level higher.
+  * eg.
+  * {
+  *   name: 'My name',
+  *   hierarchy: {
+  *     lvl0: 'Foo',
+  *     lvl1: 'Bar'
+  *   }
+  * }
+  * Will be converted to
+  * {
+  *   name: 'My name',
+  *   lvl0: 'Foo',
+  *   lvl1: 'Bar'
+  * }
+  * @param {Object} object Main object
+  * @param {String} property Main object key to move up
+  * @return {Object}
+  * @throws Error when key is not an attribute of Object or is not an object itself
+  */
+  mergeKeyWithParent(object, property) {
+    if (object[property] === undefined) {
+      return object;
+    }
+    if (typeof object[property] !== 'object') {
+      return object;
+    }
+    const newObject = $.extend({}, object, object[property]);
+    delete newObject[property];
+    return newObject;
+  },
+  /*
+  * Group all objects of a collection by the value of the specified attribute
+  * If the attribute is a string, use the lowercase form.
+  *
+  * eg.
+  * groupBy([
+  *   {name: 'Tim', category: 'dev'},
+  *   {name: 'Vincent', category: 'dev'},
+  *   {name: 'Ben', category: 'sales'},
+  *   {name: 'Jeremy', category: 'sales'},
+  *   {name: 'AlexS', category: 'dev'},
+  *   {name: 'AlexK', category: 'sales'}
+  * ], 'category');
+  * =>
+  * {
+  *   'devs': [
+  *     {name: 'Tim', category: 'dev'},
+  *     {name: 'Vincent', category: 'dev'},
+  *     {name: 'AlexS', category: 'dev'}
+  *   ],
+  *   'sales': [
+  *     {name: 'Ben', category: 'sales'},
+  *     {name: 'Jeremy', category: 'sales'},
+  *     {name: 'AlexK', category: 'sales'}
+  *   ]
+  * }
+  * @param {array} collection Array of objects to group
+  * @param {String} property The attribute on which apply the grouping
+  * @return {array}
+  * @throws Error when one of the element does not have the specified property
+  */
+  groupBy(collection, property) {
+    const newCollection = {};
+    $.each(collection, (index, item) => {
+      if (item[property] === undefined) {
+        throw new Error(`[groupBy]: Object has no key ${property}`);
+      }
+      let key = item[property];
+      if (typeof key === 'string') {
+        key = key.toLowerCase();
+      }
+      // fix #171 the given data type of docsearch hits might be conflict with the properties of the native Object,
+      // such as the constructor, so we need to do this check.
+      if (!Object.prototype.hasOwnProperty.call(newCollection, key)) {
+        newCollection[key] = [];
+      }
+      newCollection[key].push(item);
+    });
+    return newCollection;
+  },
+  /*
+  * Return an array of all the values of the specified object
+  * eg.
+  * values({
+  *   foo: 42,
+  *   bar: true,
+  *   baz: 'yep'
+  * })
+  * =>
+  * [42, true, yep]
+  * @param {object} object Object to extract values from
+  * @return {array}
+  */
+  values(object) {
+    return Object.keys(object).map(key => object[key]);
+  },
+  /*
+  * Flattens an array
+  * eg.
+  * flatten([1, 2, [3, 4], [5, 6]])
+  * =>
+  * [1, 2, 3, 4, 5, 6]
+  * @param {array} array Array to flatten
+  * @return {array}
+  */
+  flatten(array) {
+    const results = [];
+    array.forEach(value => {
+      if (!Array.isArray(value)) {
+        results.push(value);
+        return;
+      }
+      value.forEach(subvalue => {
+        results.push(subvalue);
+      });
+    });
+    return results;
+  },
+  /*
+  * Flatten all values of an object into an array, marking each first element of
+  * each group with a specific flag
+  * eg.
+  * flattenAndFlagFirst({
+  *   'devs': [
+  *     {name: 'Tim', category: 'dev'},
+  *     {name: 'Vincent', category: 'dev'},
+  *     {name: 'AlexS', category: 'dev'}
+  *   ],
+  *   'sales': [
+  *     {name: 'Ben', category: 'sales'},
+  *     {name: 'Jeremy', category: 'sales'},
+  *     {name: 'AlexK', category: 'sales'}
+  *   ]
+  * , 'isTop');
+  * =>
+  * [
+  *     {name: 'Tim', category: 'dev', isTop: true},
+  *     {name: 'Vincent', category: 'dev', isTop: false},
+  *     {name: 'AlexS', category: 'dev', isTop: false},
+  *     {name: 'Ben', category: 'sales', isTop: true},
+  *     {name: 'Jeremy', category: 'sales', isTop: false},
+  *     {name: 'AlexK', category: 'sales', isTop: false}
+  * ]
+  * @param {object} object Object to flatten
+  * @param {string} flag Flag to set to true on first element of each group
+  * @return {array}
+  */
+  flattenAndFlagFirst(object, flag) {
+    const values = this.values(object).map(collection =>
+      collection.map((item, index) => {
+        // eslint-disable-next-line no-param-reassign
+        item[flag] = index === 0;
+        return item;
+      })
+    );
+    return this.flatten(values);
+  },
+  /*
+  * Removes all empty strings, null, false and undefined elements array
+  * eg.
+  * compact([42, false, null, undefined, '', [], 'foo']);
+  * =>
+  * [42, [], 'foo']
+  * @param {array} array Array to compact
+  * @return {array}
+  */
+  compact(array) {
+    const results = [];
+    array.forEach(value => {
+      if (!value) {
+        return;
+      }
+      results.push(value);
+    });
+    return results;
+  },
+  /*
+   * Returns the highlighted value of the specified key in the specified object.
+   * If no highlighted value is available, will return the key value directly
+   * eg.
+   * getHighlightedValue({
+   *    _highlightResult: {
+   *      text: {
+   *        value: '<mark>foo</mark>'
+   *      }
+   *    },
+   *    text: 'foo'
+   * }, 'text');
+   * =>
+   * '<mark>foo</mark>'
+   * @param {object} object Hit object returned by the Algolia API
+   * @param {string} property Object key to look for
+   * @return {string}
+   **/
+  getHighlightedValue(object, property) {
+    if (
+      object._highlightResult &&
+      object._highlightResult.hierarchy_camel &&
+      object._highlightResult.hierarchy_camel[property] &&
+      object._highlightResult.hierarchy_camel[property].matchLevel &&
+      object._highlightResult.hierarchy_camel[property].matchLevel !== 'none' &&
+      object._highlightResult.hierarchy_camel[property].value
+    ) {
+      return object._highlightResult.hierarchy_camel[property].value;
+    }
+    if (
+      object._highlightResult &&
+      object._highlightResult &&
+      object._highlightResult[property] &&
+      object._highlightResult[property].value
+    ) {
+      return object._highlightResult[property].value;
+    }
+    return object[property];
+  },
+  /*
+   * Returns the snippeted value of the specified key in the specified object.
+   * If no highlighted value is available, will return the key value directly.
+   * Will add starting and ending ellipsis (…) if we detect that a sentence is
+   * incomplete
+   * eg.
+   * getSnippetedValue({
+   *    _snippetResult: {
+   *      text: {
+   *        value: '<mark>This is an unfinished sentence</mark>'
+   *      }
+   *    },
+   *    text: 'This is an unfinished sentence'
+   * }, 'text');
+   * =>
+   * '<mark>This is an unfinished sentence</mark>…'
+   * @param {object} object Hit object returned by the Algolia API
+   * @param {string} property Object key to look for
+   * @return {string}
+   **/
+  getSnippetedValue(object, property) {
+    if (
+      !object._snippetResult ||
+      !object._snippetResult[property] ||
+      !object._snippetResult[property].value
+    ) {
+      return object[property];
+    }
+    let snippet = object._snippetResult[property].value;
+
+    if (snippet[0] !== snippet[0].toUpperCase()) {
+      snippet = `…${snippet}`;
+    }
+    if (['.', '!', '?'].indexOf(snippet[snippet.length - 1]) === -1) {
+      snippet = `${snippet}…`;
+    }
+    return snippet;
+  },
+  /*
+  * Deep clone an object.
+  * Note: This will not clone functions and dates
+  * @param {object} object Object to clone
+  * @return {object}
+  */
+  deepClone(object) {
+    return JSON.parse(JSON.stringify(object));
+  },
+};
+
+export default utils;

Some files were not shown because too many files changed in this diff