1
0
mirror of https://github.com/minio/docs.git synced 2025-04-24 06:05:11 +03:00
docs/source/_static/js/instantSearch.js
Rushan ceefee34fe
Search UX enhancements (#629)
- Fix the visibility of search popup on read-mode (Prakash's comment)
- Add keyboard shortcut indicator on the read-mode search icon
<img width="254" alt="Screenshot 2022-11-04 at 17 39 05"
src="https://user-images.githubusercontent.com/13393018/199986826-e5e4ffe7-218f-4850-a0bf-867c1beb08b7.png">
2022-11-07 11:00:28 -06:00

371 lines
14 KiB
JavaScript

window.addEventListener("DOMContentLoaded", () => {
const searchToggleEl = document.querySelectorAll(".search-toggle");
const searchModalEl = document.getElementById("search");
const root = document.documentElement;
// Get the current doc platform
const activePlatform = document
.getElementsByTagName("meta")
["docsearch:platform"].getAttribute("content");
// SVG Icons
const icons = {
h1: `<svg width="14" height="9">
<path d="M1.259 9a.5.5 0 0 1-.5-.5V.773a.5.5 0 0 1 .5-.5h.845a.5.5 0 0 1 .5.5v3.001a.1.1 0 0 0 .1.1H6.25a.1.1 0 0 0 .1-.1V.773a.5.5 0 0 1 .5-.5h.841a.5.5 0 0 1 .5.5V8.5a.5.5 0 0 1-.5.5H6.85a.5.5 0 0 1-.5-.5V5.495a.1.1 0 0 0-.1-.1H2.704a.1.1 0 0 0-.1.1V8.5a.5.5 0 0 1-.5.5h-.845zM12.957.273a.5.5 0 0 1 .5.5V8.5a.5.5 0 0 1-.5.5h-.845a.5.5 0 0 1-.5-.5V2.064a.04.04 0 0 0-.04-.04h0a.04.04 0 0 0-.021.006l-1.223.767a.5.5 0 0 1-.766-.424v-.458a.5.5 0 0 1 .233-.422L11.601.35a.5.5 0 0 1 .268-.078h1.089z"/>
</svg>`,
h2: `<svg width="16" height="9">
<path d="M1.259 9a.5.5 0 0 1-.5-.5V.773a.5.5 0 0 1 .5-.5h.845a.5.5 0 0 1 .5.5v3.001a.1.1 0 0 0 .1.1H6.25a.1.1 0 0 0 .1-.1V.773a.5.5 0 0 1 .5-.5h.841a.5.5 0 0 1 .5.5V8.5a.5.5 0 0 1-.5.5H6.85a.5.5 0 0 1-.5-.5V5.495a.1.1 0 0 0-.1-.1H2.704a.1.1 0 0 0-.1.1V8.5a.5.5 0 0 1-.5.5h-.845zm8.902 0a.5.5 0 0 1-.5-.5v-.611a.5.5 0 0 1 .16-.367l2.946-2.728.665-.69a2.62 2.62 0 0 0 .413-.601 1.49 1.49 0 0 0 .141-.643 1.21 1.21 0 0 0-.175-.661c-.117-.185-.276-.331-.477-.43s-.43-.153-.686-.153c-.267 0-.5.054-.699.162a1.12 1.12 0 0 0-.46.464c-.038.071-.069.146-.094.227-.081.264-.292.493-.568.493h-.751c-.276 0-.505-.225-.464-.498.055-.366.172-.696.352-.989A2.59 2.59 0 0 1 11.05.499c.466-.23 1.003-.345 1.611-.345.625 0 1.169.111 1.632.332a2.59 2.59 0 0 1 1.087.912c.258.389.388.835.388 1.338a2.56 2.56 0 0 1-.196.976c-.131.321-.357.678-.686 1.07s-.794.857-1.393 1.402l-1.274 1.249v.06h3.165a.5.5 0 0 1 .5.5V8.5a.5.5 0 0 1-.5.5h-5.222z"/>
</svg>`,
h3: `<svg width="17" height="10">
<path d="M1.259 9a.5.5 0 0 1-.5-.5V.773a.5.5 0 0 1 .5-.5h.845a.5.5 0 0 1 .5.5v3.001a.1.1 0 0 0 .1.1H6.25a.1.1 0 0 0 .1-.1V.773a.5.5 0 0 1 .5-.5h.841a.5.5 0 0 1 .5.5V8.5a.5.5 0 0 1-.5.5H6.85a.5.5 0 0 1-.5-.5V5.495a.1.1 0 0 0-.1-.1H2.704a.1.1 0 0 0-.1.1V8.5a.5.5 0 0 1-.5.5h-.845zm11.627.119c-.636 0-1.203-.109-1.7-.328s-.885-.526-1.172-.912a2.24 2.24 0 0 1-.381-.842c-.06-.273.171-.504.451-.504h.875c.266 0 .471.221.606.45a.99.99 0 0 0 .077.112c.131.159.304.283.52.371s.459.132.729.132c.281 0 .53-.05.746-.149a1.22 1.22 0 0 0 .507-.413c.122-.176.183-.379.183-.609a1.02 1.02 0 0 0-.196-.618c-.128-.182-.312-.324-.554-.426s-.523-.153-.852-.153h-.314a.5.5 0 0 1-.5-.5v-.355a.5.5 0 0 1 .5-.5h.314a1.77 1.77 0 0 0 .737-.145c.216-.097.383-.23.503-.401a1.04 1.04 0 0 0 .179-.605c0-.219-.053-.411-.158-.575a1.04 1.04 0 0 0-.435-.392c-.185-.094-.401-.141-.648-.141a1.69 1.69 0 0 0-.686.136 1.2 1.2 0 0 0-.499.379.98.98 0 0 0-.077.119c-.13.235-.335.461-.604.461h-.779c-.279 0-.511-.232-.45-.504.067-.3.191-.575.372-.825.278-.384.653-.683 1.125-.899s1.01-.328 1.607-.328c.602 0 1.129.109 1.581.328s.803.514 1.053.886a2.15 2.15 0 0 1 .375 1.244c.003.489-.149.896-.456 1.223a2.08 2.08 0 0 1-1.189.622v.068c.642.082 1.131.305 1.466.669s.506.813.503 1.355a2.13 2.13 0 0 1-.43 1.325c-.287.386-.683.69-1.189.912s-1.085.332-1.739.332z"/>
</svg>`,
content: `<svg width="15" height="10">
<path d="M0 .75A.75.75 0 0 1 .75 0h13.5a.75.75 0 1 1 0 1.5H.75A.75.75 0 0 1 0 .75zm0 4A.75.75 0 0 1 .75 4h13.5a.75.75 0 1 1 0 1.5H.75A.75.75 0 0 1 0 4.75zm0 4A.75.75 0 0 1 .75 8h13.5a.75.75 0 1 1 0 1.5H.75A.75.75 0 0 1 0 8.75z"/>
</svg>`,
noResult: `<svg width="56" height="56">
<path d="M44.26 12.728c5.903 8.844 5.017 20.636-2.951 28.595h0L54 54 41.309 41.324c-7.722 7.709-19.803 8.94-28.924 2.948m-6.493-6.486C-.178 28.73.95 16.663 8.593 8.887S28.295-.24 37.472 5.653M4.415 51.642L51.639 4.474" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round" />
</svg>`,
chevron: `<svg viewBox="0 0 24 24" fill="none" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>`,
};
// Template for empty search results
function noResultTemplate(query) {
return `<div class="search__empty">
${icons.noResult}</br>
No results have been found for <span>${query}</span>.</br>
Please rephrase your query!
</div>`;
}
// Detect macOS
function isMac() {
return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
}
// Algolia credintials
const algoliaClient = algoliasearch(
"E1CSOK3UC2",
"6bc246d81fd3b79f51cf88f0b2481bac"
);
// By default, Instantsearch will display the latest hits even without a query.
// As per our design, we don't need this behaviour.
// So, use promise to conditionally render the search results.
const searchClient = {
...algoliaClient,
search(requests) {
if (requests.every(({ params }) => !params.query)) {
return Promise.resolve({
results: requests.map(() => ({
hits: [],
nbHits: 0,
nbPages: 0,
page: 0,
processingTimeMS: 0,
hitsPerPage: 0,
exhaustiveNbHits: false,
query: "",
params: "",
})),
});
}
return algoliaClient.search(requests);
},
};
// Init Instantsearch
const search = instantsearch({
indexName: "minio",
searchClient,
initialUiState: {
minio: {
refinementList: {
platform: [activePlatform],
},
},
},
});
// Add Instanstsearch widgets
search.addWidgets([
instantsearch.widgets.searchBox({
container: "#search-box",
autofocus: true,
showReset: false,
showSubmit: false,
placeholder: "Search Documentation",
cssClasses: {
input: "search__input",
reset: "search__reset",
form: "search__form",
loadingIndicator: "search__loading",
},
queryHook(query, search) {
if (query !== "") {
// Make search modal active
searchModalEl.classList.add("search--focused");
// Clear the filters and select the active platform
setTimeout(() => {
const activeFilterEl = document.querySelector(
".search__filters__checkbox[value='" + activePlatform + "']"
);
if (activeFilterEl && !activeFilterEl.checked) {
activeFilterEl.click();
}
}, 50);
} else {
// Clear the filters
clearRefinements();
}
// Clear the filters on x click
document
.querySelector(".search__reset")
.addEventListener("click", () => {
clearRefinements("btn");
});
// Fire the search
search(query);
},
}),
instantsearch.widgets.poweredBy({
container: "#search-powered-by",
cssClasses: {
link: "search__powered-by",
},
}),
instantsearch.widgets.refinementList({
container: "#search-filters",
attribute: "platform",
cssClasses: {
root: "search__filters",
noRefinementRoot: "search__filters--empty",
list: "search__filters__list",
labelText: "search__filters__label",
checkbox: "search__filters__checkbox",
count: "search__filters__count",
},
}),
instantsearch.widgets.clearRefinements({
container: "#search-clear",
cssClasses: {
button: "search-clear__btn",
},
}),
instantsearch.widgets.hits({
container: "#search-results",
cssClasses: {
root: "search__hits",
emptyRoot: "search__hits--empty",
list: "search__hits__list",
item: "search__hits__item",
},
templates: {
empty: function (data) {
return data.query !== "" ? noResultTemplate(data.query) : "";
},
item: function (data) {
var returnString;
var docUrl;
var refinedLenth =
search.renderState.minio.refinementList.platform.items.filter(
(x) => x.isRefined
).length;
if (refinedLenth !== 1) {
searchModalEl.classList.add("search--show-platform");
} else {
searchModalEl.classList.remove("search--show-platform");
}
// If the query is a full-match of a lvl1 title,
// display only the h1.
if (
data.hierarchy.lvl1 &&
data._highlightResult.hierarchy.lvl1.matchLevel === "full"
) {
docUrl = data.url_without_anchor;
returnString = `
<i class="search__hits__icon">
${icons.h1}
</i>
<div class="search__hits__title">${data._highlightResult.hierarchy.lvl1.value}</div>
<div class="search__hits__platform">${data.platform}</div>
`;
}
// If the query is a full-match of a lvl2 title,
// display h2 as the title and h1 as the sub-text.
else if (
data.hierarchy.lvl2 &&
data._highlightResult.hierarchy.lvl2.matchLevel === "full"
) {
docUrl = data.url;
returnString = `
<i class="search__hits__icon">
${icons.h2}
</i>
<div class="search__hits__title">${data._highlightResult.hierarchy.lvl2.value}</div>
<div class="search__hits__label">
<div class="search__hits__platform">${data.platform}</div>
${data.hierarchy.lvl1}
</div>
`;
}
// If the query is a full-match of a lvl3 title,
// display h3 as the title and h1, h2 as the sub-text.
else if (
data.hierarchy.lvl3 &&
data._highlightResult.hierarchy.lvl3.matchLevel === "full"
) {
docUrl = data.url;
returnString = `
<i class="search__hits__icon">
${icons.h3}
</i>
<div class="search__hits__title">${data._highlightResult.hierarchy.lvl3.value}</div>
<div class="search__hits__label">
<div class="search__hits__platform">${data.platform}</div>
${data.hierarchy.lvl1}
</div>
`;
}
// If the query is a full-match of any content,
// display the content as the title and h1, h2? and h3? as the sub-text.
else if (
data.hierarchy.lvl1 &&
data._snippetResult &&
data._snippetResult.content.matchLevel !== "none"
) {
docUrl = data.url;
returnString = `
<i class="search__hits__icon">
${icons.content}
</i>
<div class="search__hits__title">${
data._snippetResult.content.value
}</div>
<div class="search__hits__label">
<div class="search__hits__platform">${data.platform}</div>
<span>${data.hierarchy.lvl1}</span>
${
data.hierarchy.lvl2
? `${icons.chevron}` +
`<span>${data.hierarchy.lvl2}</span>`
: ""
}
${
data.hierarchy.lvl3
? `${icons.chevron}` +
`<span>${data.hierarchy.lvl3}</span>`
: ""
}
</div>
`;
} else {
return false;
}
return `<a target="_blank" href="${docUrl}">
${returnString}
</a>`;
},
},
}),
]);
// Function to clear search field and filters
function clearRefinements(trigger) {
const filtersClearEl = document.querySelector(
"#search-clear .search-clear__btn"
);
filtersClearEl?.click();
const removeClasses = function () {
root.classList.remove("locked");
searchModalEl.classList.remove("search--active", "search--focused");
};
if (trigger === "btn") {
removeClasses();
} else {
if (!root.classList.contains("read-mode")) {
removeClasses();
}
}
}
// Function to close and reset the search modal
function closeSearchModal() {
const searchResetEl = document.querySelector(".search__reset");
setTimeout(() => {
searchResetEl.click();
});
}
// Toggle search modal on read-mode and mobile
// on search icon click.
searchToggleEl.forEach((item) => {
item.addEventListener("click", (e) => {
e.preventDefault();
root.classList.add("locked");
searchModalEl.classList.add("search--active");
setTimeout(() => {
const instaSearchInputEl = document.querySelector(".search__input");
instaSearchInputEl.focus();
});
});
});
// Keyboard events
document.addEventListener(
"keydown",
(e) => {
// Close the search on esc key press
if (e.key === "Escape") {
closeSearchModal();
}
// Focus the search input on "Meta + K" key press
const searchInputEl = document.querySelector(".search__input");
var metaKey = isMac() ? e.metaKey : e.ctrlKey;
if (metaKey && e.key === "k") {
if (root.classList.contains("read-mode")) {
searchModalEl.classList.add("search--active");
searchInputEl.focus();
}
searchInputEl.focus();
window.scrollTo(0, 0);
}
},
false
);
// Start the search
search.start();
// --------------------------------------------------
// Trigger search on keyboard shortcut
// meta + k
// --------------------------------------------------
const searchEl = document.getElementById("search-box");
const searchToggleBtnEl = document.querySelector(".search-toggle--keyboard");
if (searchEl) {
var metaKey = isMac() ? "⌘K" : "Ctrl+K";
[searchEl, searchToggleBtnEl].forEach((item) => {
item.insertAdjacentHTML("beforeend", `<i class="search-meta-key">${metaKey}</i>`);
});
}
});