mirror of
https://github.com/docker/docs.git
synced 2026-03-27 14:28:47 +07:00
fix: search bar (#23459)
Fix for intermittent bug that happens on the docs site when using the search bar/search drop-down, reported by Eng - Fixed disconnected dropdown to keep dropdown attached to search bar - Added viewport boundary detection for window resizing - Made search icon clickable (critical for mobile) Tested on: - Chrome - Safari - Mobile https://docker.atlassian.net/browse/ENGDOCS-3002
This commit is contained in:
@@ -1,185 +1,191 @@
|
||||
{{ define "left" }}
|
||||
{{ partial "sidebar/mainnav.html" . }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<article class="prose dark:prose-invert max-w-none">
|
||||
<h1 class="py-4">{{ .Title }}</h1>
|
||||
{{ .Content }}
|
||||
<div class="not-prose">
|
||||
<div class="flex justify-between gap-8 flex-row">
|
||||
<input type="search" id="search-page-input"
|
||||
class="ring-3-gray-light-200 dark:ring-3-gray-dark-400 dark:bg-background-dark focus:ring-3-blue-light dark:focus:ring-3-blue-dark ring-3-[1.5px] w-full max-w-xl min-w-0 rounded-sm bg-white px-4 py-2 outline-hidden"
|
||||
placeholder="Search…" tabindex="0" />
|
||||
<div class="admonition flex flex-row items-center gap-1">
|
||||
<p>Not finding what you're looking for? Try</p>
|
||||
<button onclick="askAI('search-page-input')" class="topbar-button bg-blue-400/95 border-blue-300 hover:bg-blue-400/90">
|
||||
<span>Ask AI</span>
|
||||
<span class="icon-svg">
|
||||
{{ partial "utils/svg.html" "/icons/sparkle.svg" }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-divider-light dark:border-divider-dark" />
|
||||
<div id="search-page-results">
|
||||
<!-- results -->
|
||||
{{ define "left" }}{{ partial "sidebar/mainnav.html" . }}{{ end }} {{ define
|
||||
"main" }}
|
||||
<article class="prose dark:prose-invert max-w-none">
|
||||
<h1 class="py-4">{{ .Title }}</h1>
|
||||
{{ .Content }}
|
||||
<div class="not-prose">
|
||||
<div class="flex flex-col items-center justify-between gap-8 lg:flex-row">
|
||||
<input
|
||||
type="search"
|
||||
id="search-page-input"
|
||||
class="ring-3-gray-light-200 dark:ring-3-gray-dark-400 dark:bg-background-dark focus:ring-3-blue-light dark:focus:ring-3-blue-dark ring-3-[1.5px] outline-hidden w-full min-w-0 rounded-sm bg-white px-4 py-2 lg:max-w-xl"
|
||||
placeholder="Search…"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Not finding what you're looking for? Try
|
||||
</p>
|
||||
<button
|
||||
onclick="askAI('search-page-input')"
|
||||
class="topbar-button open-kapa-widget"
|
||||
>
|
||||
<span>Ask AI</span>
|
||||
<span class="icon-svg">
|
||||
{{ partial "utils/svg.html" "/icons/sparkle.svg" }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<script type="module">
|
||||
// Global variable to hold the pagefind module
|
||||
let pagefind;
|
||||
<hr class="border-divider-light dark:border-divider-dark" />
|
||||
<div id="search-page-results">
|
||||
<!-- results -->
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<script type="module">
|
||||
// Global variable to hold the pagefind module
|
||||
let pagefind;
|
||||
|
||||
// Initialize the pagefind module and fire a search if the query parameter exists
|
||||
window.addEventListener("load", async function () {
|
||||
// Hydrate pagefind
|
||||
pagefind = await import("/pagefind/pagefind.js");
|
||||
await pagefind.options({
|
||||
ranking: {
|
||||
termFrequency: 0.2,
|
||||
pageLength: 0.75,
|
||||
termSaturation: 1.4,
|
||||
termSimilarity: 6.0,
|
||||
},
|
||||
});
|
||||
|
||||
// Get the query parameter from the URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const query = urlParams.get("q");
|
||||
|
||||
// If no query parameter is set, return
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById("search-page-input");
|
||||
|
||||
// Set the value of the input field to the query parameter
|
||||
searchInput.value = query;
|
||||
|
||||
// Trigger the input event to simulate user typing
|
||||
const event = new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
// Trigger the input event for the search input
|
||||
searchInput.dispatchEvent(event);
|
||||
searchInput.focus();
|
||||
// Initialize the pagefind module and fire a search if the query parameter exists
|
||||
window.addEventListener("load", async function () {
|
||||
// Hydrate pagefind
|
||||
pagefind = await import("/pagefind/pagefind.js");
|
||||
await pagefind.options({
|
||||
ranking: {
|
||||
termFrequency: 0.2,
|
||||
pageLength: 0.75,
|
||||
termSaturation: 1.4,
|
||||
termSimilarity: 6.0,
|
||||
},
|
||||
});
|
||||
|
||||
const searchPageInput = document.querySelector("#search-page-input");
|
||||
const searchPageResults = document.querySelector("#search-page-results");
|
||||
// Get the query parameter from the URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const query = urlParams.get("q");
|
||||
|
||||
// onPageSearch returns 10 results per query
|
||||
async function onPageSearch(e) {
|
||||
pagefind.init();
|
||||
const query = e.target.value;
|
||||
// If no query parameter is set, return
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the query parameter in the URL
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
params.set("q", query);
|
||||
const searchInput = document.getElementById("search-page-input");
|
||||
|
||||
// Default the current page to 1
|
||||
let currentPage = 1;
|
||||
// Set the value of the input field to the query parameter
|
||||
searchInput.value = query;
|
||||
|
||||
// Check if the page parameter exists
|
||||
const page = params.get("page");
|
||||
// Calculate the range start based on the page parameter
|
||||
if (page) {
|
||||
currentPage = parseInt(page);
|
||||
}
|
||||
const rangeStart = (currentPage - 1) * 10;
|
||||
const rangeEnd = rangeStart + 10;
|
||||
// Trigger the input event to simulate user typing
|
||||
const event = new Event("input", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
// Trigger the input event for the search input
|
||||
searchInput.dispatchEvent(event);
|
||||
searchInput.focus();
|
||||
});
|
||||
|
||||
// Execute the search
|
||||
const search = await pagefind.debouncedSearch(query);
|
||||
// If no search results are found, exit
|
||||
if (search === null) {
|
||||
const searchPageInput = document.querySelector("#search-page-input");
|
||||
const searchPageResults = document.querySelector("#search-page-results");
|
||||
|
||||
// onPageSearch returns 10 results per query
|
||||
async function onPageSearch(e) {
|
||||
pagefind.init();
|
||||
const query = e.target.value;
|
||||
|
||||
// Set the query parameter in the URL
|
||||
const params = new URLSearchParams(document.location.search);
|
||||
params.set("q", query);
|
||||
|
||||
// Default the current page to 1
|
||||
let currentPage = 1;
|
||||
|
||||
// Check if the page parameter exists
|
||||
const page = params.get("page");
|
||||
// Calculate the range start based on the page parameter
|
||||
if (page) {
|
||||
currentPage = parseInt(page);
|
||||
}
|
||||
const rangeStart = (currentPage - 1) * 10;
|
||||
const rangeEnd = rangeStart + 10;
|
||||
|
||||
// Execute the search
|
||||
const search = await pagefind.debouncedSearch(query);
|
||||
// If no search results are found, exit
|
||||
if (search === null) {
|
||||
return;
|
||||
} else {
|
||||
// total number of results
|
||||
const resultsLength = search.results.length;
|
||||
// Get the data for the search results
|
||||
// Slice the results based on the range start + 10
|
||||
const resultsData = await Promise.all(
|
||||
search.results.slice(rangeStart, rangeEnd).map((r) => r.data()),
|
||||
);
|
||||
// If the range does not have any results, display a message
|
||||
if (resultsData.length === 0) {
|
||||
searchPageResults.innerHTML = `<div class="p-4">No results found</div>`;
|
||||
return;
|
||||
}
|
||||
// Add an index to the results, for heap tracking
|
||||
const results = resultsData.map((item, index) => ({
|
||||
...item,
|
||||
index: index + 1,
|
||||
}));
|
||||
|
||||
// If the query is not empty, display the search results container
|
||||
if (query) {
|
||||
searchPageResults.classList.remove("hidden");
|
||||
} else {
|
||||
// total number of results
|
||||
const resultsLength = search.results.length;
|
||||
// Get the data for the search results
|
||||
// Slice the results based on the range start + 10
|
||||
const resultsData = await Promise.all(
|
||||
search.results.slice(rangeStart, rangeEnd).map((r) => r.data()),
|
||||
);
|
||||
// If the range does not have any results, display a message
|
||||
if (resultsData.length === 0) {
|
||||
searchPageResults.innerHTML = `<div class="p-4">No results found</div>`;
|
||||
return;
|
||||
}
|
||||
// Add an index to the results, for heap tracking
|
||||
const results = resultsData.map((item, index) => ({
|
||||
...item,
|
||||
index: index + 1,
|
||||
}));
|
||||
searchPageResults.classList.add("hidden");
|
||||
}
|
||||
|
||||
// If the query is not empty, display the search results container
|
||||
if (query) {
|
||||
searchPageResults.classList.remove("hidden");
|
||||
} else {
|
||||
searchPageResults.classList.add("hidden");
|
||||
}
|
||||
// Generate the search results HTML
|
||||
let resultsHTML = `<div class="text-gray-400 dark:text-gray-500 p-2">${resultsLength} results</div>`;
|
||||
|
||||
// Generate the search results HTML
|
||||
let resultsHTML = `<div class="text-gray-400 dark:text-gray-500 p-2">${resultsLength} results</div>`;
|
||||
|
||||
// Map results to HTML
|
||||
resultsHTML += results
|
||||
.map((item) => {
|
||||
return `<div class="p-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-gray-400 dark:texty-gray-dark text-sm">${item.meta.breadcrumbs}</span>
|
||||
<a class="link" href="${item.url}" data-query="${query}" data-index="${item.index}">${item.meta.title}</a>
|
||||
<p class="text-black dark:text-white overflow-hidden">…${item.excerpt}…</p>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
// If the results length is greater than 10, display links to show more results
|
||||
if (resultsLength > 10) {
|
||||
resultsHTML += `<hr class="border-divider-light dark:border-divider-dark">`;
|
||||
resultsHTML += `<ul class="flex flex-wrap gap-1 pt-4 pb-8 justify-center text-sm">`;
|
||||
for (let i = 1; i <= resultsLength / 10; i++) {
|
||||
if (i == currentPage) {
|
||||
resultsHTML += `<li class="flex items-center justify-center">
|
||||
// Map results to HTML
|
||||
resultsHTML += results
|
||||
.map((item) => {
|
||||
return `<div class="p-4">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-gray-400 dark:texty-gray-dark text-sm">${item.meta.breadcrumbs}</span>
|
||||
<a class="link" style="word-break: break-word; overflow-wrap: anywhere;" href="${item.url}" data-query="${query}" data-index="${item.index}">${item.meta.title}</a>
|
||||
<p class="text-black dark:text-white overflow-hidden" style="word-break: break-word; overflow-wrap: anywhere;">…${item.excerpt}…</p>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
// If the results length is greater than 10, display links to show more results
|
||||
if (resultsLength > 10) {
|
||||
resultsHTML += `<hr class="border-divider-light dark:border-divider-dark">`;
|
||||
resultsHTML += `<ul class="flex flex-wrap gap-1 pt-4 pb-8 justify-center text-sm">`;
|
||||
for (let i = 1; i <= resultsLength / 10; i++) {
|
||||
if (i == currentPage) {
|
||||
resultsHTML += `<li class="flex items-center justify-center">
|
||||
<a href="/search/?q=${query}&page=${i}" class="pagination-link bg-gray-200 dark:bg-gray-800 dark:text-gray-200">${i}</a>
|
||||
</li>`;
|
||||
} else {
|
||||
resultsHTML += `<li class="flex items-center justify-center">
|
||||
} else {
|
||||
resultsHTML += `<li class="flex items-center justify-center">
|
||||
<a href="/search/?q=${query}&page=${i}" class="pagination-link bg-gray-100 dark:bg-gray-900 dark:text-gray-400">${i}</a>
|
||||
</li>`;
|
||||
}
|
||||
}
|
||||
resultsHTML += `</ul>`;
|
||||
}
|
||||
|
||||
searchPageResults.innerHTML = resultsHTML;
|
||||
resultsHTML += `</ul>`;
|
||||
}
|
||||
}
|
||||
|
||||
searchPageInput.addEventListener("input", (e) => onPageSearch(e));
|
||||
|
||||
// Event delegation for tracking link clicks
|
||||
if (window.heap !== undefined) {
|
||||
searchPageResults.addEventListener("click", function (event) {
|
||||
if (event.target.tagName === "A" && event.target.closest(".link")) {
|
||||
const searchQuery = event.target.getAttribute("data-query");
|
||||
const resultIndex = event.target.getAttribute("data-index");
|
||||
const url = new URL(event.target.href);
|
||||
const properties = {
|
||||
docs_search_target_path: url.pathname,
|
||||
docs_search_target_title: event.target.textContent,
|
||||
docs_search_query_text: searchQuery,
|
||||
docs_search_target_index: resultIndex,
|
||||
docs_search_source_path: window.location.pathname,
|
||||
docs_search_source_title: document.title,
|
||||
};
|
||||
heap.track("Docs - Search - Click - Result Link", properties);
|
||||
}
|
||||
});
|
||||
searchPageResults.innerHTML = resultsHTML;
|
||||
}
|
||||
</script>
|
||||
}
|
||||
|
||||
searchPageInput.addEventListener("input", (e) => onPageSearch(e));
|
||||
|
||||
// Event delegation for tracking link clicks
|
||||
if (window.heap !== undefined) {
|
||||
searchPageResults.addEventListener("click", function (event) {
|
||||
if (event.target.tagName === "A" && event.target.closest(".link")) {
|
||||
const searchQuery = event.target.getAttribute("data-query");
|
||||
const resultIndex = event.target.getAttribute("data-index");
|
||||
const url = new URL(event.target.href);
|
||||
const properties = {
|
||||
docs_search_target_path: url.pathname,
|
||||
docs_search_target_title: event.target.textContent,
|
||||
docs_search_query_text: searchQuery,
|
||||
docs_search_target_index: resultIndex,
|
||||
docs_search_source_path: window.location.pathname,
|
||||
docs_search_source_title: document.title,
|
||||
};
|
||||
heap.track("Docs - Search - Click - Result Link", properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<div
|
||||
x-ref="searchBarRef"
|
||||
x-data="{ open: false }"
|
||||
@click.outside="open = false;"
|
||||
@keyup.escape.window="open = false"
|
||||
id="search-bar"
|
||||
class="relative items-center flex w-full max-w-full overflow-x-auto"
|
||||
>
|
||||
<input
|
||||
class="border-none outline-none focus:outline-none min-w-0"
|
||||
x-ref="searchBarInput"
|
||||
type="search"
|
||||
id="search-bar-input"
|
||||
placeholder="Search"
|
||||
@focus="open = true;"
|
||||
@keyup.enter.prevent="window.location.href = '/search/?q=' + $event.target.value;"
|
||||
@keyup.escape.prevent="open = false;"
|
||||
@keydown.window="(e) => {
|
||||
x-ref="searchBarRef"
|
||||
x-data="{ open: false }"
|
||||
@click.outside="open = false;"
|
||||
@keyup.escape.window="open = false"
|
||||
id="search-bar"
|
||||
class="relative flex w-full max-w-full items-center overflow-visible"
|
||||
>
|
||||
<input
|
||||
class="min-w-0 border-none outline-none focus:outline-none"
|
||||
x-ref="searchBarInput"
|
||||
type="search"
|
||||
id="search-bar-input"
|
||||
placeholder="Search"
|
||||
@focus="open = true;"
|
||||
@keyup.enter.prevent="window.location.href = '/search/?q=' + $event.target.value;"
|
||||
@keyup.escape.prevent="open = false;"
|
||||
@keydown.window="(e) => {
|
||||
switch(e.key) {
|
||||
case 'k':
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
@@ -25,108 +25,176 @@
|
||||
break;
|
||||
}
|
||||
}"
|
||||
tabindex="0"
|
||||
/>
|
||||
<div id="search-bar-icon">
|
||||
<span class="icon-svg-stroke">
|
||||
{{ partial "utils/svg.html" "/icons/search.svg" }}
|
||||
</span>
|
||||
</div>
|
||||
<div id="search-bar-dropdown"
|
||||
x-show="open"
|
||||
x-cloak
|
||||
x-ref="dropdown"
|
||||
class="hidden md:block font-medium text-gray-400 dark:text-gray-200 bg-gray-50 dark:bg-gray-900 rounded-sm mt-4 border-1 border-gray-100 dark:border-gray-700 fixed z-[999] w-[500px] p-6 shadow-md"
|
||||
x-effect="if (open) {
|
||||
const containerRect = document.getElementById('search-bar-container').getBoundingClientRect();
|
||||
tabindex="0"
|
||||
/>
|
||||
<button
|
||||
id="search-bar-icon"
|
||||
@click="window.location.href = '/search/?q=' + $refs.searchBarInput.value;"
|
||||
class="cursor-pointer transition-opacity hover:opacity-80"
|
||||
aria-label="Search"
|
||||
>
|
||||
<span class="icon-svg-stroke">
|
||||
{{ partial "utils/svg.html" "/icons/search.svg" }}
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
id="search-bar-dropdown"
|
||||
x-show="open"
|
||||
x-cloak
|
||||
x-ref="dropdown"
|
||||
class="border-1 fixed z-[999] mt-2 hidden rounded-sm border-gray-100 bg-gray-50 p-6 font-medium text-gray-400 shadow-md md:block dark:border-gray-700 dark:bg-gray-900 dark:text-gray-200"
|
||||
style="width: min(500px, 90vw)"
|
||||
x-effect="if (open) {
|
||||
const rect = $refs.searchBarRef.getBoundingClientRect();
|
||||
$el.style.top = (rect.bottom + window.scrollY - 2) + 'px';
|
||||
$el.style.right = (window.innerWidth - containerRect.right - 15) + 'px';
|
||||
}">
|
||||
<div id="search-bar-results">
|
||||
{{- $emptyState := `<div>Start typing to search or try <button onclick="askAI('search-bar-input')" class="link">Ask
|
||||
AI</button>.</div>` }}
|
||||
{{- $emptyState | safe.HTML }}
|
||||
<!-- results -->
|
||||
const dropdownWidth = Math.min(500, window.innerWidth * 0.9);
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
// Calculate left position
|
||||
let leftPos = rect.left;
|
||||
|
||||
// Prevent going off right edge
|
||||
if (leftPos + dropdownWidth > viewportWidth - 20) {
|
||||
leftPos = viewportWidth - dropdownWidth - 20;
|
||||
}
|
||||
|
||||
// Prevent going off left edge
|
||||
if (leftPos < 20) {
|
||||
leftPos = 20;
|
||||
}
|
||||
|
||||
$el.style.top = (rect.bottom + 8) + 'px';
|
||||
$el.style.left = leftPos + 'px';
|
||||
}"
|
||||
>
|
||||
<div id="search-bar-results">
|
||||
{{- $emptyState := `
|
||||
<div>
|
||||
Start typing to search or try
|
||||
<button onclick="askAI('search-bar-input')" class="link">Ask AI</button
|
||||
>.
|
||||
</div>
|
||||
` }} {{- $emptyState | safe.HTML }}
|
||||
<!-- results -->
|
||||
</div>
|
||||
<script type="module">
|
||||
window.addEventListener("load", async function () {
|
||||
const pagefind = await import("/pagefind/pagefind.js");
|
||||
await pagefind.options({
|
||||
ranking: {
|
||||
termFrequency: 0.2,
|
||||
pageLength: 0.75,
|
||||
termSaturation: 1.4,
|
||||
termSimilarity: 6.0
|
||||
},
|
||||
});
|
||||
|
||||
const searchBarInput = document.querySelector("#search-bar-input");
|
||||
const searchBarResults = document.querySelector(
|
||||
"#search-bar-results",
|
||||
);
|
||||
|
||||
async function search(e) {
|
||||
const query = e.target.value;
|
||||
if (query === "") {
|
||||
searchBarResults.innerHTML = `{{ $emptyState | safe.HTML }}`;
|
||||
return;
|
||||
}
|
||||
const search = await pagefind.debouncedSearch(query);
|
||||
if (search === null) {
|
||||
return;
|
||||
} else {
|
||||
const resultsLength = search.results.length
|
||||
const resultsData = await Promise.all(search.results.slice(0, 5).map(r => r.data()));
|
||||
const results = resultsData.map((item, index) => ({...item, index: index + 1}));
|
||||
|
||||
if (query) {
|
||||
searchBarResults.classList.remove("hidden");
|
||||
} else {
|
||||
searchBarResults.classList.add("hidden");
|
||||
}
|
||||
|
||||
let resultsHTML = `<div class="p-2 text-gray-400 dark:text-gray-500">${resultsLength} results</div>`;
|
||||
resultsHTML += results
|
||||
.map((item) => {
|
||||
return `<div class="p-2">
|
||||
<div class="flex flex-col items-start item">
|
||||
<a class="link" href="${item.url}" data-query="${query}" data-index="${item.index}">${item.meta.title}</a>
|
||||
<p class="text-black dark:text-white overflow-hidden text-left">…${item.excerpt}…</p>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
if (resultsLength > 5) {
|
||||
resultsHTML += `<div class="w-fit ml-auto px-4 py-2"><a href="/search/?q=${query}" class="link">Show all results</a></div>`;
|
||||
}
|
||||
|
||||
searchBarResults.innerHTML = resultsHTML;
|
||||
}
|
||||
}
|
||||
|
||||
searchBarInput.addEventListener("input", search);
|
||||
|
||||
// Event delegation for tracking link clicks
|
||||
if (window.heap !== undefined) {
|
||||
searchBarResults.addEventListener('click', function (event) {
|
||||
if (event.target.tagName === 'A' && event.target.closest('.link')) {
|
||||
const searchQuery = event.target.getAttribute('data-query');
|
||||
const resultIndex = event.target.getAttribute('data-index');
|
||||
const url = new URL(event.target.href);
|
||||
const properties = {
|
||||
docs_search_target_path: url.pathname,
|
||||
docs_search_target_title: event.target.textContent,
|
||||
docs_search_query_text: searchQuery,
|
||||
docs_search_target_index: resultIndex,
|
||||
docs_search_source_path: window.location.pathname,
|
||||
docs_search_source_title: document.title,
|
||||
};
|
||||
heap.track("Docs - Search - Click - Result Link", properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
</div>
|
||||
<script type="module">
|
||||
window.addEventListener("load", async function () {
|
||||
const pagefind = await import("/pagefind/pagefind.js");
|
||||
await pagefind.options({
|
||||
ranking: {
|
||||
termFrequency: 0.2,
|
||||
pageLength: 0.75,
|
||||
termSaturation: 1.4,
|
||||
termSimilarity: 6.0,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
const searchBarInput = document.querySelector("#search-bar-input");
|
||||
const searchBarResults = document.querySelector("#search-bar-results");
|
||||
const searchDropdown = document.querySelector("#search-bar-dropdown");
|
||||
|
||||
// Update position on scroll and resize
|
||||
function updateDropdownPosition() {
|
||||
const searchBar = document.querySelector("#search-bar");
|
||||
if (
|
||||
!searchBar ||
|
||||
!searchDropdown ||
|
||||
searchDropdown.style.display === "none"
|
||||
)
|
||||
return;
|
||||
|
||||
const rect = searchBar.getBoundingClientRect();
|
||||
const dropdownWidth = Math.min(500, window.innerWidth * 0.9);
|
||||
const viewportWidth = window.innerWidth;
|
||||
|
||||
let leftPos = rect.left;
|
||||
|
||||
if (leftPos + dropdownWidth > viewportWidth - 20) {
|
||||
leftPos = viewportWidth - dropdownWidth - 20;
|
||||
}
|
||||
|
||||
if (leftPos < 20) {
|
||||
leftPos = 20;
|
||||
}
|
||||
|
||||
searchDropdown.style.top = rect.bottom + 8 + "px";
|
||||
searchDropdown.style.left = leftPos + "px";
|
||||
}
|
||||
|
||||
window.addEventListener("scroll", updateDropdownPosition);
|
||||
window.addEventListener("resize", updateDropdownPosition);
|
||||
|
||||
async function search(e) {
|
||||
const query = e.target.value;
|
||||
if (query === "") {
|
||||
searchBarResults.innerHTML = `{{ $emptyState | safe.HTML }}`;
|
||||
return;
|
||||
}
|
||||
const search = await pagefind.debouncedSearch(query);
|
||||
if (search === null) {
|
||||
return;
|
||||
} else {
|
||||
const resultsLength = search.results.length;
|
||||
const resultsData = await Promise.all(
|
||||
search.results.slice(0, 5).map((r) => r.data()),
|
||||
);
|
||||
const results = resultsData.map((item, index) => ({
|
||||
...item,
|
||||
index: index + 1,
|
||||
}));
|
||||
|
||||
if (query) {
|
||||
searchBarResults.classList.remove("hidden");
|
||||
} else {
|
||||
searchBarResults.classList.add("hidden");
|
||||
}
|
||||
|
||||
let resultsHTML = `<div class="p-2 text-gray-400 dark:text-gray-500">${resultsLength} results</div>`;
|
||||
resultsHTML += results
|
||||
.map((item) => {
|
||||
// Truncate excerpt if it's too long
|
||||
let excerpt = item.excerpt;
|
||||
if (excerpt.length > 200) {
|
||||
excerpt = excerpt.substring(0, 200);
|
||||
}
|
||||
return `<div class="p-2">
|
||||
<div class="flex flex-col items-start item">
|
||||
<a class="link" style="word-break: break-word; overflow-wrap: anywhere;" href="${item.url}" data-query="${query}" data-index="${item.index}">${item.meta.title}</a>
|
||||
<p class="text-black dark:text-white overflow-hidden text-left" style="word-break: break-word; overflow-wrap: anywhere;">…${excerpt}…</p>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
if (resultsLength > 5) {
|
||||
resultsHTML += `<div class="w-fit ml-auto px-4 py-2"><a href="/search/?q=${query}" class="link">Show all results</a></div>`;
|
||||
}
|
||||
|
||||
searchBarResults.innerHTML = resultsHTML;
|
||||
}
|
||||
}
|
||||
|
||||
searchBarInput.addEventListener("input", search);
|
||||
|
||||
// Event delegation for tracking link clicks
|
||||
if (window.heap !== undefined) {
|
||||
searchBarResults.addEventListener("click", function (event) {
|
||||
if (event.target.tagName === "A" && event.target.closest(".link")) {
|
||||
const searchQuery = event.target.getAttribute("data-query");
|
||||
const resultIndex = event.target.getAttribute("data-index");
|
||||
const url = new URL(event.target.href);
|
||||
const properties = {
|
||||
docs_search_target_path: url.pathname,
|
||||
docs_search_target_title: event.target.textContent,
|
||||
docs_search_query_text: searchQuery,
|
||||
docs_search_target_index: resultIndex,
|
||||
docs_search_source_path: window.location.pathname,
|
||||
docs_search_source_title: document.title,
|
||||
};
|
||||
heap.track("Docs - Search - Click - Result Link", properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user