fix: add search input
This commit is contained in:
@@ -1,21 +1,119 @@
|
||||
/**
|
||||
* Search Bar Component
|
||||
* Search input with icon
|
||||
* Search input with mic and search icons on the right
|
||||
* Includes speech recognition functionality
|
||||
*/
|
||||
import { micIcon } from "../icons/mic.js";
|
||||
import { searchIcon } from "../icons/search.js";
|
||||
|
||||
class SearchBar extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.recognition = null;
|
||||
this.isListening = false;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.addEventListeners();
|
||||
this.initSpeechRecognition();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.recognition) {
|
||||
this.recognition.stop();
|
||||
}
|
||||
}
|
||||
|
||||
initSpeechRecognition() {
|
||||
// Check for browser support
|
||||
const SpeechRecognition =
|
||||
window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
if (!SpeechRecognition) {
|
||||
console.warn("Speech recognition is not supported in this browser");
|
||||
const micButton = this.shadowRoot.querySelector(".mic-button");
|
||||
if (micButton) {
|
||||
micButton.style.display = "none";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.recognition = new SpeechRecognition();
|
||||
this.recognition.continuous = false;
|
||||
this.recognition.interimResults = true;
|
||||
this.recognition.lang = "nl-NL";
|
||||
|
||||
this.recognition.onstart = () => {
|
||||
this.isListening = true;
|
||||
this.updateMicButtonState();
|
||||
};
|
||||
|
||||
this.recognition.onend = () => {
|
||||
this.isListening = false;
|
||||
this.updateMicButtonState();
|
||||
};
|
||||
|
||||
this.recognition.onresult = (event) => {
|
||||
const input = this.shadowRoot.querySelector(".search-input");
|
||||
const transcript = Array.from(event.results)
|
||||
.map((result) => result[0].transcript)
|
||||
.join("");
|
||||
|
||||
input.value = transcript;
|
||||
|
||||
// Dispatch input event for live updates
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("search-input", {
|
||||
detail: { query: transcript },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
|
||||
// If final result, also dispatch search event
|
||||
if (event.results[0].isFinal) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("search", {
|
||||
detail: { query: transcript },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
this.recognition.onerror = (event) => {
|
||||
console.error("Speech recognition error:", event.error);
|
||||
this.isListening = false;
|
||||
this.updateMicButtonState();
|
||||
};
|
||||
}
|
||||
|
||||
updateMicButtonState() {
|
||||
const micButton = this.shadowRoot.querySelector(".mic-button");
|
||||
if (micButton) {
|
||||
micButton.classList.toggle("listening", this.isListening);
|
||||
micButton.setAttribute("aria-pressed", this.isListening.toString());
|
||||
}
|
||||
}
|
||||
|
||||
toggleSpeechRecognition() {
|
||||
if (!this.recognition) return;
|
||||
|
||||
if (this.isListening) {
|
||||
this.recognition.stop();
|
||||
} else {
|
||||
this.recognition.start();
|
||||
}
|
||||
}
|
||||
|
||||
addEventListeners() {
|
||||
const input = this.shadowRoot.querySelector(".search-input");
|
||||
const form = this.shadowRoot.querySelector(".search-form");
|
||||
const micButton = this.shadowRoot.querySelector(".mic-button");
|
||||
const searchButton = this.shadowRoot.querySelector(".search-button");
|
||||
|
||||
form.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
@@ -37,9 +135,25 @@ class SearchBar extends HTMLElement {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
micButton.addEventListener("click", () => {
|
||||
this.toggleSpeechRecognition();
|
||||
});
|
||||
|
||||
searchButton.addEventListener("click", () => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("search", {
|
||||
detail: { query: input.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconColor = "#951D51";
|
||||
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
@@ -49,53 +163,115 @@ class SearchBar extends HTMLElement {
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-sm, 0.5rem);
|
||||
height: 50px;
|
||||
padding: 8px 16px;
|
||||
background-color: #951D51;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: var(--spacing-md, 1rem);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--color-text-light, #64748b);
|
||||
pointer-events: none;
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
background-color: #FADCE7;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
||||
padding-left: calc(var(--spacing-md, 1rem) + 20px + var(--spacing-sm, 0.5rem));
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
color: var(--color-text, #1e293b);
|
||||
background-color: var(--color-background-tertiary, #f1f5f9);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-lg, 0.75rem);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
font-family: 'Outfit', system-ui, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
color: #383838;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all var(--transition-fast, 150ms ease);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--color-text-light, #64748b);
|
||||
font-family: 'Outfit', system-ui, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
color: rgba(56, 56, 56, 0.5);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
background-color: var(--color-background, #ffffff);
|
||||
border-color: var(--color-primary, #2563eb);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
.icon-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
transition: background-color 150ms ease;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background-color: rgba(149, 29, 81, 0.1);
|
||||
}
|
||||
|
||||
.icon-button:active {
|
||||
background-color: rgba(149, 29, 81, 0.2);
|
||||
}
|
||||
|
||||
.icon-button.listening {
|
||||
background-color: rgba(149, 29, 81, 0.2);
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
<form class="search-form" role="search">
|
||||
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
class="search-input"
|
||||
placeholder="Search books, authors..."
|
||||
aria-label="Search books and authors"
|
||||
>
|
||||
<div class="search-container">
|
||||
<input
|
||||
type="search"
|
||||
class="search-input"
|
||||
placeholder="Waar ben je naar op zoek?"
|
||||
aria-label="Zoek boeken en auteurs"
|
||||
>
|
||||
<div class="icon-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-button mic-button"
|
||||
aria-label="Zoeken met spraak"
|
||||
aria-pressed="false"
|
||||
>
|
||||
${micIcon({ size: 20, color: iconColor })}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="icon-button search-button"
|
||||
aria-label="Zoeken"
|
||||
>
|
||||
${searchIcon({ size: 20, color: iconColor })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user