281 lines
7.0 KiB
JavaScript
281 lines
7.0 KiB
JavaScript
/**
|
|
* Search Bar Component
|
|
* 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();
|
|
this.dispatchEvent(
|
|
new CustomEvent("search", {
|
|
detail: { query: input.value },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
});
|
|
|
|
input.addEventListener("input", (e) => {
|
|
this.dispatchEvent(
|
|
new CustomEvent("search-input", {
|
|
detail: { query: e.target.value },
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
});
|
|
|
|
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 {
|
|
display: block;
|
|
}
|
|
|
|
.search-form {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 50px;
|
|
padding: 8px 16px;
|
|
background-color: #951D51;
|
|
}
|
|
|
|
.search-container {
|
|
display: flex;
|
|
align-items: center;
|
|
width: 100%;
|
|
height: 40px;
|
|
background-color: #FFF;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.search-input {
|
|
flex: 1;
|
|
height: 100%;
|
|
padding: 0 16px;
|
|
font-family: var(--font-family-base);
|
|
font-size: var(--font-size-base, 16px);
|
|
font-weight: var(--font-weight-light, 300);
|
|
color: #383838;
|
|
background-color: transparent;
|
|
border: none;
|
|
outline: none;
|
|
}
|
|
|
|
.search-input::placeholder {
|
|
font-family: var(--font-family-base);
|
|
font-size: var(--font-size-base, 16px);
|
|
font-weight: var(--font-weight-light, 300);
|
|
color: rgba(56, 56, 56, 0.5);
|
|
}
|
|
|
|
.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">
|
|
<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>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define("search-bar", SearchBar);
|