Global Search
This commit is contained in:
@@ -52,6 +52,221 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-search-wrapper {
|
||||
position: relative;
|
||||
width: min(500px, 100%);
|
||||
}
|
||||
|
||||
.header-search {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
|
||||
background: #020617;
|
||||
color: var(--text);
|
||||
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
|
||||
padding: 10px 18px;
|
||||
|
||||
font: inherit;
|
||||
outline: none;
|
||||
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
background 0.18s ease;
|
||||
}
|
||||
|
||||
.header-search:hover {
|
||||
border-color: #475569;
|
||||
}
|
||||
|
||||
.header-search:focus {
|
||||
background: #07101f;
|
||||
border-color: var(--accent);
|
||||
box-shadow:
|
||||
0 0 0 3px var(--accent-soft),
|
||||
0 12px 30px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.header-search::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.guide-search-results {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
max-height: min(520px, calc(100vh - 120px));
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
|
||||
padding: 7px;
|
||||
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
backdrop-filter: blur(18px);
|
||||
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
|
||||
box-shadow:
|
||||
0 22px 60px rgba(0, 0, 0, 0.45),
|
||||
0 4px 14px rgba(0, 0, 0, 0.25);
|
||||
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
.guide-search-results[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.guide-search-result {
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
|
||||
padding: 11px 12px;
|
||||
|
||||
color: var(--text);
|
||||
background: transparent;
|
||||
|
||||
border: 0;
|
||||
border-radius: 11px;
|
||||
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
transition:
|
||||
background 0.14s ease,
|
||||
transform 0.14s ease;
|
||||
}
|
||||
|
||||
.guide-search-result:hover,
|
||||
.guide-search-result.is-active {
|
||||
background: rgba(59, 130, 246, 0.16);
|
||||
}
|
||||
|
||||
.guide-search-result:active {
|
||||
transform: scale(0.992);
|
||||
}
|
||||
|
||||
.guide-search-result.is-current {
|
||||
background: rgba(34, 197, 94, 0.08);
|
||||
}
|
||||
|
||||
.guide-search-result.is-current:hover,
|
||||
.guide-search-result.is-current.is-active {
|
||||
background: rgba(34, 197, 94, 0.17);
|
||||
}
|
||||
|
||||
.guide-search-type-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
padding-top: 1px;
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.guide-search-result-content {
|
||||
min-width: 0;
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.guide-search-result-content strong {
|
||||
overflow: hidden;
|
||||
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.guide-search-result-content > span {
|
||||
overflow: hidden;
|
||||
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.guide-search-result-content .guide-search-subtitle {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.guide-search-channel {
|
||||
border-bottom: 1px solid rgba(51, 65, 85, 0.55);
|
||||
}
|
||||
|
||||
.guide-search-message {
|
||||
padding: 18px 14px;
|
||||
|
||||
color: var(--muted);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.guide-search-message.is-loading::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
margin-right: 9px;
|
||||
|
||||
vertical-align: -2px;
|
||||
|
||||
border: 2px solid rgba(148, 163, 184, 0.35);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
|
||||
animation: guide-search-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
.guide-search-message.is-error {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.search-channel-highlight {
|
||||
animation: search-channel-highlight 1.6s ease;
|
||||
}
|
||||
|
||||
@keyframes guide-search-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes search-channel-highlight {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
25%,
|
||||
70% {
|
||||
box-shadow:
|
||||
inset 0 0 0 2px var(--accent),
|
||||
0 0 24px var(--accent-soft);
|
||||
}
|
||||
}
|
||||
|
||||
.header-search {
|
||||
width: min(500px, 100%);
|
||||
background: #020617;
|
||||
@@ -135,6 +350,14 @@
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.header-search-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.guide-search-results {
|
||||
max-height: min(430px, calc(100vh - 190px));
|
||||
}
|
||||
|
||||
.header-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,7 @@ export function initProgrammeModal() {
|
||||
const logo = document.getElementById("modalLogo");
|
||||
const channel = document.getElementById("modalChannel");
|
||||
|
||||
const openModal = (programmeElement) => {
|
||||
const id = programmeElement.dataset.programmeId;
|
||||
const programme = window.TVGUIDE_PROGRAMMES?.[id];
|
||||
|
||||
const openProgramme = (programme) => {
|
||||
if (!programme) {
|
||||
return;
|
||||
}
|
||||
@@ -25,77 +22,129 @@ export function initProgrammeModal() {
|
||||
const metadata = programme.metadata || {};
|
||||
|
||||
title.textContent = programme.title || "";
|
||||
|
||||
subtitle.textContent =
|
||||
metadata.episode_title ||
|
||||
programme.subtitle ||
|
||||
"";
|
||||
description.textContent = programme.description || "Keine Beschreibung vorhanden.";
|
||||
metadata.episode_title ||
|
||||
programme.subtitle ||
|
||||
"";
|
||||
|
||||
subtitle.hidden = !subtitle.textContent;
|
||||
|
||||
description.textContent =
|
||||
programme.description ||
|
||||
"Keine Beschreibung vorhanden.";
|
||||
|
||||
category.textContent = programme.category || "";
|
||||
category.hidden = !category.textContent;
|
||||
|
||||
channel.textContent =
|
||||
`Kanal ${programme.channel_number} · ${programme.channel}`;
|
||||
const channelName =
|
||||
programme.channel ||
|
||||
programme.channel_name ||
|
||||
"";
|
||||
|
||||
logo.src = `/assets/logos/${encodeURIComponent(programme.channel_id)}.svg`;
|
||||
const channelNumber = programme.channel_number;
|
||||
|
||||
channel.textContent = channelNumber && channelNumber !== 999999
|
||||
? `Kanal ${channelNumber} · ${channelName}`
|
||||
: channelName;
|
||||
|
||||
logo.src =
|
||||
`/assets/logos/${encodeURIComponent(programme.channel_id || "")}.svg`;
|
||||
|
||||
logo.onerror = () => {
|
||||
logo.onerror = null;
|
||||
logo.src = "/assets/logos/default.svg";
|
||||
logo.onerror = null;
|
||||
logo.src = "/assets/logos/default.svg";
|
||||
};
|
||||
|
||||
const metaItems = [];
|
||||
|
||||
if (metadata.season && metadata.episode) {
|
||||
let text = `Staffel ${metadata.season} · Episode ${metadata.episode}`;
|
||||
let text =
|
||||
`Staffel ${metadata.season} · Episode ${metadata.episode}`;
|
||||
|
||||
if (metadata.episode_total) {
|
||||
text += `/${metadata.episode_total}`;
|
||||
}
|
||||
if (metadata.episode_total) {
|
||||
text += `/${metadata.episode_total}`;
|
||||
}
|
||||
|
||||
metaItems.push(text);
|
||||
metaItems.push(text);
|
||||
}
|
||||
|
||||
if (metadata.country) {
|
||||
metaItems.push(metadata.country);
|
||||
metaItems.push(metadata.country);
|
||||
} else if (programme.country) {
|
||||
metaItems.push(programme.country);
|
||||
}
|
||||
|
||||
if (metadata.year) {
|
||||
metaItems.push(metadata.year);
|
||||
metaItems.push(metadata.year);
|
||||
} else if (programme.year) {
|
||||
metaItems.push(programme.year);
|
||||
}
|
||||
|
||||
if (metadata.age_rating) {
|
||||
metaItems.push(`FSK ${metadata.age_rating}`);
|
||||
metaItems.push(`FSK ${metadata.age_rating}`);
|
||||
} else if (programme.rating) {
|
||||
metaItems.push(`FSK ${programme.rating}`);
|
||||
}
|
||||
|
||||
if (Array.isArray(metadata.genres) && metadata.genres.length) {
|
||||
metaItems.push(metadata.genres.join(", "));
|
||||
metaItems.push(metadata.genres.join(", "));
|
||||
}
|
||||
|
||||
meta.textContent = metaItems.join(" • ");
|
||||
meta.hidden = metaItems.length === 0;
|
||||
|
||||
time.textContent = `${programme.start} – ${programme.stop}`;
|
||||
const start =
|
||||
programme.start_time ||
|
||||
programme.start ||
|
||||
"";
|
||||
|
||||
const stop =
|
||||
programme.stop_time ||
|
||||
programme.stop ||
|
||||
"";
|
||||
|
||||
time.textContent = start && stop
|
||||
? `${start} – ${stop}`
|
||||
: "";
|
||||
|
||||
modal.hidden = false;
|
||||
document.body.classList.add("modal-open");
|
||||
};
|
||||
|
||||
const openProgrammeElement = (programmeElement) => {
|
||||
const id = programmeElement.dataset.programmeId;
|
||||
const programme = window.TVGUIDE_PROGRAMMES?.[id];
|
||||
|
||||
openProgramme(programme);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
modal.hidden = true;
|
||||
meta.textContent = "";
|
||||
document.body.classList.remove("modal-open");
|
||||
};
|
||||
|
||||
/*
|
||||
* Öffentliche Schnittstelle für die globale Suche.
|
||||
*/
|
||||
window.TVGuideProgrammeModal = {
|
||||
open: openProgramme,
|
||||
close: closeModal
|
||||
};
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
const programme = event.target.closest(".timeline-programme, .dashboard-programme");
|
||||
const programme = event.target.closest(
|
||||
".timeline-programme, .dashboard-programme"
|
||||
);
|
||||
|
||||
if (!programme) {
|
||||
return;
|
||||
}
|
||||
|
||||
openModal(programme);
|
||||
openProgrammeElement(programme);
|
||||
});
|
||||
|
||||
|
||||
modal.querySelectorAll("[data-modal-close]").forEach((element) => {
|
||||
element.addEventListener("click", closeModal);
|
||||
});
|
||||
|
||||
@@ -1,72 +1,424 @@
|
||||
export function initGuideSearch() {
|
||||
const wrapper = document.querySelector("[data-global-search]");
|
||||
const search = document.getElementById("guideSearch");
|
||||
const results = document.getElementById("guideSearchResults");
|
||||
const programmes = window.TVGUIDE_PROGRAMMES || {};
|
||||
|
||||
if (!search || !results) {
|
||||
if (!wrapper || !search || !results) {
|
||||
return;
|
||||
}
|
||||
|
||||
const openProgrammeById = (id) => {
|
||||
const element = document.querySelector(`[data-programme-id="${CSS.escape(id)}"]`);
|
||||
let debounceTimer = null;
|
||||
let requestController = null;
|
||||
let activeIndex = -1;
|
||||
let currentItems = [];
|
||||
|
||||
if (element) {
|
||||
element.click();
|
||||
}
|
||||
const escapeText = (value) => String(value ?? "");
|
||||
|
||||
const closeResults = () => {
|
||||
results.hidden = true;
|
||||
results.innerHTML = "";
|
||||
search.setAttribute("aria-expanded", "false");
|
||||
|
||||
currentItems = [];
|
||||
activeIndex = -1;
|
||||
};
|
||||
|
||||
search.addEventListener("input", () => {
|
||||
const query = search.value.trim().toLowerCase();
|
||||
|
||||
if (query.length < 2) {
|
||||
results.hidden = true;
|
||||
results.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = Object.values(programmes)
|
||||
.filter((programme) => {
|
||||
return [
|
||||
programme.title,
|
||||
programme.subtitle,
|
||||
programme.description,
|
||||
programme.category,
|
||||
programme.channel
|
||||
].some((value) => (value || "").toLowerCase().includes(query));
|
||||
})
|
||||
.slice(0, 12);
|
||||
|
||||
if (!matches.length) {
|
||||
results.innerHTML = `<div class="guide-search-result"><strong>Nichts gefunden</strong></div>`;
|
||||
results.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
results.innerHTML = matches.map((programme) => `
|
||||
<div class="guide-search-result" data-search-programme-id="${programme.id}">
|
||||
<strong>${programme.title || "Ohne Titel"}</strong>
|
||||
<span>${programme.channel || ""} · ${programme.start || ""} – ${programme.stop || ""}</span>
|
||||
</div>
|
||||
`).join("");
|
||||
|
||||
const openResults = () => {
|
||||
results.hidden = false;
|
||||
});
|
||||
search.setAttribute("aria-expanded", "true");
|
||||
};
|
||||
|
||||
results.addEventListener("click", (event) => {
|
||||
const item = event.target.closest("[data-search-programme-id]");
|
||||
const setMessage = (message, className = "") => {
|
||||
results.innerHTML = "";
|
||||
|
||||
const element = document.createElement("div");
|
||||
element.className = `guide-search-message ${className}`.trim();
|
||||
element.textContent = message;
|
||||
|
||||
results.appendChild(element);
|
||||
openResults();
|
||||
|
||||
currentItems = [];
|
||||
activeIndex = -1;
|
||||
};
|
||||
|
||||
const getResultElements = () => {
|
||||
return Array.from(
|
||||
results.querySelectorAll("[data-search-index]")
|
||||
);
|
||||
};
|
||||
|
||||
const updateActiveResult = () => {
|
||||
const elements = getResultElements();
|
||||
|
||||
elements.forEach((element, index) => {
|
||||
const active = index === activeIndex;
|
||||
|
||||
element.classList.toggle("is-active", active);
|
||||
element.setAttribute(
|
||||
"aria-selected",
|
||||
active ? "true" : "false"
|
||||
);
|
||||
|
||||
if (active) {
|
||||
element.scrollIntoView({
|
||||
block: "nearest"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectResult = (index) => {
|
||||
const item = currentItems[index];
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
openProgrammeById(item.dataset.searchProgrammeId);
|
||||
results.hidden = true;
|
||||
search.value = "";
|
||||
if (item.type === "programme") {
|
||||
window.TVGuideProgrammeModal?.open(item);
|
||||
|
||||
search.value = "";
|
||||
closeResults();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.type === "channel") {
|
||||
openChannel(item);
|
||||
}
|
||||
};
|
||||
|
||||
const openChannel = (item) => {
|
||||
const channelId = String(item.channel_id || "");
|
||||
|
||||
/*
|
||||
* Auf der Tagesansicht zuerst versuchen, den vorhandenen Sender
|
||||
* direkt anzuspringen.
|
||||
*/
|
||||
const channelElement =
|
||||
document.querySelector(
|
||||
`[data-channel-id="${CSS.escape(channelId)}"]`
|
||||
) ||
|
||||
document.querySelector(
|
||||
`[data-timeline-channel="${CSS.escape(channelId)}"]`
|
||||
);
|
||||
|
||||
if (channelElement) {
|
||||
channelElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
inline: "start"
|
||||
});
|
||||
|
||||
channelElement.classList.add("search-channel-highlight");
|
||||
|
||||
window.setTimeout(() => {
|
||||
channelElement.classList.remove("search-channel-highlight");
|
||||
}, 1600);
|
||||
|
||||
search.value = "";
|
||||
closeResults();
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Falls der Sender auf der aktuellen Ansicht nicht im DOM liegt,
|
||||
* Tagesansicht mit Senderfilter öffnen.
|
||||
*/
|
||||
window.location.href =
|
||||
`/?view=day&channel=${encodeURIComponent(channelId)}`;
|
||||
};
|
||||
|
||||
const createChannelResult = (item, index) => {
|
||||
const button = document.createElement("button");
|
||||
|
||||
button.type = "button";
|
||||
button.className =
|
||||
"guide-search-result guide-search-channel";
|
||||
|
||||
button.dataset.searchIndex = String(index);
|
||||
button.setAttribute("role", "option");
|
||||
button.setAttribute("aria-selected", "false");
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "guide-search-type-icon";
|
||||
icon.textContent = "📺";
|
||||
|
||||
const content = document.createElement("span");
|
||||
content.className = "guide-search-result-content";
|
||||
|
||||
const heading = document.createElement("strong");
|
||||
heading.textContent =
|
||||
escapeText(item.channel_name || "Unbekannter Sender");
|
||||
|
||||
const details = document.createElement("span");
|
||||
|
||||
const number =
|
||||
item.channel_number &&
|
||||
item.channel_number !== 999999
|
||||
? `Kanal ${item.channel_number}`
|
||||
: "Sender";
|
||||
|
||||
details.textContent =
|
||||
item.favorite
|
||||
? `${number} · Favorit ⭐`
|
||||
: number;
|
||||
|
||||
content.append(heading, details);
|
||||
button.append(icon, content);
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
const createProgrammeResult = (item, index) => {
|
||||
const button = document.createElement("button");
|
||||
|
||||
button.type = "button";
|
||||
button.className =
|
||||
"guide-search-result guide-search-programme";
|
||||
|
||||
if (item.is_current) {
|
||||
button.classList.add("is-current");
|
||||
}
|
||||
|
||||
button.dataset.searchIndex = String(index);
|
||||
button.setAttribute("role", "option");
|
||||
button.setAttribute("aria-selected", "false");
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "guide-search-type-icon";
|
||||
icon.textContent = item.is_current ? "🔴" : "🎬";
|
||||
|
||||
const content = document.createElement("span");
|
||||
content.className = "guide-search-result-content";
|
||||
|
||||
const heading = document.createElement("strong");
|
||||
heading.textContent =
|
||||
escapeText(item.title || "Ohne Titel");
|
||||
|
||||
const details = document.createElement("span");
|
||||
|
||||
const parts = [
|
||||
item.channel_name || "",
|
||||
item.start_time && item.stop_time
|
||||
? `${item.start_time}–${item.stop_time}`
|
||||
: ""
|
||||
];
|
||||
|
||||
if (item.is_current) {
|
||||
parts.push("läuft jetzt");
|
||||
}
|
||||
|
||||
details.textContent = parts.filter(Boolean).join(" · ");
|
||||
|
||||
content.append(heading);
|
||||
|
||||
if (item.subtitle) {
|
||||
const subtitle = document.createElement("span");
|
||||
subtitle.className = "guide-search-subtitle";
|
||||
subtitle.textContent = escapeText(item.subtitle);
|
||||
content.append(subtitle);
|
||||
}
|
||||
|
||||
content.append(details);
|
||||
button.append(icon, content);
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
const renderResults = (items) => {
|
||||
results.innerHTML = "";
|
||||
currentItems = items;
|
||||
activeIndex = -1;
|
||||
|
||||
if (!items.length) {
|
||||
setMessage(
|
||||
"Keine passenden Sender oder Sendungen gefunden."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const element =
|
||||
item.type === "channel"
|
||||
? createChannelResult(item, index)
|
||||
: createProgrammeResult(item, index);
|
||||
|
||||
results.appendChild(element);
|
||||
});
|
||||
|
||||
openResults();
|
||||
};
|
||||
|
||||
const performSearch = async () => {
|
||||
const query = search.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
closeResults();
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestController) {
|
||||
requestController.abort();
|
||||
}
|
||||
|
||||
requestController = new AbortController();
|
||||
|
||||
setMessage("Suche läuft …", "is-loading");
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/?api=search&q=${encodeURIComponent(query)}&limit=20`,
|
||||
{
|
||||
method: "GET",
|
||||
cache: "no-store",
|
||||
signal: requestController.signal,
|
||||
headers: {
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
/*
|
||||
* Verhindert, dass ein älteres Ergebnis nach einer inzwischen
|
||||
* geänderten Eingabe angezeigt wird.
|
||||
*/
|
||||
if (search.value.trim() !== query) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.ok) {
|
||||
throw new Error(data.error || "Unbekannter Fehler");
|
||||
}
|
||||
|
||||
renderResults(
|
||||
Array.isArray(data.results)
|
||||
? data.results
|
||||
: []
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
|
||||
console.error("Programmsuche fehlgeschlagen:", error);
|
||||
|
||||
setMessage(
|
||||
"Die Suche konnte gerade nicht geladen werden.",
|
||||
"is-error"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
search.addEventListener("input", () => {
|
||||
window.clearTimeout(debounceTimer);
|
||||
|
||||
const query = search.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
if (requestController) {
|
||||
requestController.abort();
|
||||
}
|
||||
|
||||
closeResults();
|
||||
return;
|
||||
}
|
||||
|
||||
debounceTimer = window.setTimeout(
|
||||
performSearch,
|
||||
300
|
||||
);
|
||||
});
|
||||
|
||||
search.addEventListener("focus", () => {
|
||||
if (
|
||||
search.value.trim().length >= 2 &&
|
||||
results.children.length
|
||||
) {
|
||||
openResults();
|
||||
}
|
||||
});
|
||||
|
||||
search.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeResults();
|
||||
search.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
if (results.hidden || !currentItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
|
||||
activeIndex =
|
||||
activeIndex < currentItems.length - 1
|
||||
? activeIndex + 1
|
||||
: 0;
|
||||
|
||||
updateActiveResult();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
|
||||
activeIndex =
|
||||
activeIndex > 0
|
||||
? activeIndex - 1
|
||||
: currentItems.length - 1;
|
||||
|
||||
updateActiveResult();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
if (activeIndex < 0) {
|
||||
activeIndex = 0;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
selectResult(activeIndex);
|
||||
}
|
||||
});
|
||||
|
||||
results.addEventListener("mousemove", (event) => {
|
||||
const element =
|
||||
event.target.closest("[data-search-index]");
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeIndex = Number(element.dataset.searchIndex);
|
||||
updateActiveResult();
|
||||
});
|
||||
|
||||
results.addEventListener("click", (event) => {
|
||||
const element =
|
||||
event.target.closest("[data-search-index]");
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectResult(
|
||||
Number(element.dataset.searchIndex)
|
||||
);
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!event.target.closest(".guide-search")) {
|
||||
results.hidden = true;
|
||||
if (!wrapper.contains(event.target)) {
|
||||
closeResults();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user