-
-
diff --git a/web/src/App.js b/web/src/App.js
index 4b6c3ed..7ab3d7f 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -138,7 +138,7 @@ export class App {
setGenerating(isGenerating) {
this.playerState.setGenerating(isGenerating);
this.elements.generateBtn.disabled = isGenerating;
- this.elements.generateBtn.className = isGenerating ? 'loading' : '';
+ this.elements.generateBtn.classList.toggle('loading', isGenerating);
this.elements.generateBtnLoader.style.display = isGenerating ? 'block' : 'none';
this.elements.generateBtnText.style.visibility = isGenerating ? 'hidden' : 'visible';
this.elements.cancelBtn.style.display = isGenerating ? 'block' : 'none';
@@ -227,4 +227,4 @@ export class App {
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new App();
-});
\ No newline at end of file
+});
diff --git a/web/src/components/VoiceSelector.js b/web/src/components/VoiceSelector.js
index 4499d2f..65ce3b6 100644
--- a/web/src/components/VoiceSelector.js
+++ b/web/src/components/VoiceSelector.js
@@ -12,25 +12,45 @@ export class VoiceSelector {
}
setupEventListeners() {
+ // Voice search focus
+ this.elements.voiceSearch.addEventListener('focus', () => {
+ this.elements.voiceDropdown.classList.add('show');
+ });
+
// Voice search
this.elements.voiceSearch.addEventListener('input', (e) => {
const filteredVoices = this.voiceService.filterVoices(e.target.value);
this.renderVoiceOptions(filteredVoices);
});
- // Voice selection
- this.elements.voiceOptions.addEventListener('change', (e) => {
- if (e.target.type === 'checkbox') {
- if (e.target.checked) {
- this.voiceService.addVoice(e.target.value);
- } else {
- this.voiceService.removeVoice(e.target.value);
- }
- this.updateSelectedVoicesDisplay();
+ // Voice selection - handle clicks on the entire voice option
+ this.elements.voiceOptions.addEventListener('mousedown', (e) => {
+ e.preventDefault(); // Prevent blur on search input
+
+ const voiceOption = e.target.closest('.voice-option');
+ if (!voiceOption) return;
+
+ const voice = voiceOption.dataset.voice;
+ if (!voice) return;
+
+ const isSelected = voiceOption.classList.contains('selected');
+
+ if (!isSelected) {
+ this.voiceService.addVoice(voice);
+ } else {
+ this.voiceService.removeVoice(voice);
}
+
+ voiceOption.classList.toggle('selected');
+ this.updateSelectedVoicesDisplay();
+
+ // Keep focus on search input
+ requestAnimationFrame(() => {
+ this.elements.voiceSearch.focus();
+ });
});
- // Weight adjustment and voice removal
+ // Weight adjustment
this.elements.selectedVoices.addEventListener('input', (e) => {
if (e.target.type === 'number') {
const voice = e.target.dataset.voice;
@@ -47,24 +67,30 @@ export class VoiceSelector {
// Remove selected voice
this.elements.selectedVoices.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-voice')) {
+ e.preventDefault();
+ e.stopPropagation();
const voice = e.target.dataset.voice;
this.voiceService.removeVoice(voice);
- this.updateVoiceCheckbox(voice, false);
+ this.updateVoiceOptionState(voice, false);
this.updateSelectedVoicesDisplay();
}
});
- // Dropdown visibility
- this.elements.voiceSearch.addEventListener('focus', () => {
- this.elements.voiceDropdown.style.display = 'block';
- this.updateSearchPlaceholder();
- });
-
- document.addEventListener('click', (e) => {
- if (!this.elements.voiceSearch.contains(e.target) &&
- !this.elements.voiceDropdown.contains(e.target)) {
- this.elements.voiceDropdown.style.display = 'none';
+ // Handle clicks outside to close dropdown
+ document.addEventListener('mousedown', (e) => {
+ // Don't handle clicks in selected voices area
+ if (this.elements.selectedVoices.contains(e.target)) {
+ return;
}
+
+ // Don't close if clicking in search or dropdown
+ if (this.elements.voiceSearch.contains(e.target) ||
+ this.elements.voiceDropdown.contains(e.target)) {
+ return;
+ }
+
+ this.elements.voiceDropdown.classList.remove('show');
+ this.elements.voiceSearch.blur();
});
this.elements.voiceSearch.addEventListener('blur', () => {
@@ -77,11 +103,10 @@ export class VoiceSelector {
renderVoiceOptions(voices) {
this.elements.voiceOptions.innerHTML = voices
.map(voice => `
-