fix & feat: resolve user reports #1 through #8 (TTS voice & speed, modals, scrollbar, CEFR hint, SRS grammar exclusion, global word lookup)

This commit is contained in:
Antigravity committed 2026-10-08 12:37:41 +03:00
1 parent 8cffae46fa
commit 7871191c90
12 files changed
+443 -103

No files matched your search

+64 -4
View File
@@ -7,7 +7,7 @@ import { api } from './modules/api.js';
import { initTheme, openModal, closeModal, showToast, playTextSpeech, switchView } from './modules/ui.js';
import { initDictionary, loadInitialData } from './modules/dictionary.js';
import { initSrs, startSrsTrainingSession, openStatsDashboard } from './modules/srs.js';
import { initReader, loadBookshelf, openBook } from './modules/reader.js';
import { initReader, loadBookshelf, openBook, openWordPopup, extractContextSentence } from './modules/reader.js';
import { initCefr, loadCefrOverview, startCefrQuiz } from './modules/cefr.js';
import { initGrammar, loadGrammarUnits, openGrammarUnit } from './modules/grammar.js';
import { initLessonModule, openLessonConfigurator } from './modules/lesson.js';
@@ -416,13 +416,73 @@ export function initNavigationDrawer() {
if (drawerCloseBtn) drawerCloseBtn.addEventListener('click', closeDrawer);
if (drawerBackdrop) drawerBackdrop.addEventListener('click', closeDrawer);
// Escape Key Support
// Global Escape Key & Modal Dismiss Support
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && drawer && drawer.classList.contains('active')) {
closeDrawer();
if (e.key === 'Escape') {
// 1. If navigation drawer is open, close it
if (drawer && drawer.classList.contains('active')) {
closeDrawer();
return;
}
// 2. Close any open modal backdrop (topmost visible modal)
const openModals = Array.from(document.querySelectorAll('.modal-backdrop:not(.hidden)'));
if (openModals.length > 0) {
const topModal = openModals[openModals.length - 1];
topModal.classList.add('hidden');
}
}
});
// Dismiss modals when clicking on dark backdrop outside modal window
document.addEventListener('click', (e) => {
if (e.target && e.target.classList && e.target.classList.contains('modal-backdrop')) {
e.target.classList.add('hidden');
}
});
// -----------------------------------------------------------------------
// Global Text Selection Lookup & Add-to-Dictionary (Anywhere on Page)
// -----------------------------------------------------------------------
function handleGlobalTextSelection(e) {
if (e && e.target && e.target.closest('#reader-word-popup, button, input, textarea, select, .modal-window, #feedback-widget, .icon-btn, .opt-btn, .tab-item, .drawer-nav-item')) {
return;
}
const selection = window.getSelection();
if (!selection || selection.isCollapsed) return;
const raw = selection.toString().trim();
if (!raw || raw.length < 2 || raw.length > 50) return;
// Clean punctuation / quotes
const clean = raw.replace(/^['"“«`\s\-_.,;:!?()[\]{}]+|['"”»`\s\-_.,;:!?()[\]{}]+$/g, '').trim();
// Require Latin characters (English vocabulary)
if (!clean || !/^[A-Za-z\s'-]+$/.test(clean)) return;
if (selection.rangeCount === 0) return;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
if (rect.width === 0 && rect.height === 0) return;
const containerElem = range.startContainer?.nodeType === 1 ? range.startContainer : range.startContainer?.parentElement;
// Avoid re-triggering if already inside reader word span (which handles its own click)
if (containerElem?.closest('.reader-word')) return;
const fullText = containerElem?.innerText || containerElem?.textContent || '';
const contextSentence = extractContextSentence(fullText, clean);
const virtualTarget = {
getBoundingClientRect: () => range.getBoundingClientRect(),
isConnected: true
};
openWordPopup(clean, contextSentence, virtualTarget, fullText);
}
document.addEventListener('mouseup', handleGlobalTextSelection);
document.addEventListener('touchend', (e) => {
setTimeout(() => handleGlobalTextSelection(e), 150);
});
// Touch Swipe Left to Dismiss (Mobile & Tablet)
let touchStartX = 0;
let touchCurrentX = 0;