diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/content.js b/content.js index aad87e7..295f59f 100644 --- a/content.js +++ b/content.js @@ -1,3 +1,15 @@ +// Import utilities +import { + createAnnouncer, + announceToScreenReader as announce, + getISOCode, + NOTIFICATION_COLORS, + Z_INDEX_MAX, + NOTIFICATION_DURATION, + NOTIFICATION_FADE_DURATION, + removeElementById +} from './utils.js'; + // 1. STATE & VARIABLES let currentMode = null; let activeHoverElement = null; @@ -6,70 +18,77 @@ let isSaving = false; let isLocked = false; let shortcutCache = []; -// 2. ACCESSIBILITY ENGINE -const srAnnouncer = document.createElement('div'); -srAnnouncer.id = "webkeybind-announcer"; -srAnnouncer.setAttribute('aria-live', 'assertive'); -srAnnouncer.setAttribute('aria-atomic', 'true'); -srAnnouncer.style.cssText = 'position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap;'; -document.body.appendChild(srAnnouncer); +// 2. DOM CACHE +const domCache = { + announcer: null, + notification: null, + shadowRoot: null +}; + +// Initialize announcer +domCache.announcer = createAnnouncer('webkeybind-announcer'); +document.body.appendChild(domCache.announcer); +// 3. ACCESSIBILITY ENGINE function announceToScreenReader(message, color = "default") { showNotification(message, color); - const langMap = { "English": "en", "हिंदी": "hi", "मराठी": "mr", "മലയാളം": "ml" }; - const isoCode = langMap[window.currentLang] || "en"; - srAnnouncer.setAttribute('lang', isoCode); - - srAnnouncer.textContent = ''; - setTimeout(() => { srAnnouncer.textContent = message; }, 50); + const isoCode = getISOCode(window.currentLang || "English"); + announce(domCache.announcer, message, isoCode); } function showNotification(msg, colorType) { if (colorType === "modal") return; - const existing = document.getElementById('webkeybind-notification'); - if (existing) existing.remove(); + // Remove existing notification using cache + if (domCache.notification && domCache.notification.parentNode) { + domCache.notification.remove(); + domCache.notification = null; + } const div = document.createElement('div'); div.id = 'webkeybind-notification'; div.innerText = msg; div.setAttribute('aria-hidden', 'true'); - let bgColor = "#333333"; - if (colorType === "blue") bgColor = "#007BFF"; - if (colorType === "purple") bgColor = "#6f42c1"; - if (colorType === "orange") bgColor = "#FF9800"; - if (colorType === "red") bgColor = "#DC3545"; - if (colorType === "green") bgColor = "#28A745"; + const bgColor = NOTIFICATION_COLORS[colorType] || NOTIFICATION_COLORS.default; div.style.cssText = ` position: fixed !important; top: 20px !important; left: 50% !important; transform: translateX(-50%) !important; background-color: ${bgColor} !important; color: white !important; padding: 12px 24px !important; border-radius: 8px !important; - z-index: 2147483647 !important; font-family: sans-serif !important; + z-index: ${Z_INDEX_MAX} !important; font-family: sans-serif !important; font-weight: bold !important; font-size: 16px !important; box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important; transition: opacity 0.3s ease-in-out !important; pointer-events: none; `; document.body.appendChild(div); + domCache.notification = div; + setTimeout(() => { if (div && div.parentNode) { div.style.opacity = "0"; - setTimeout(() => { if (div.parentNode) div.remove(); }, 300); + setTimeout(() => { + if (div.parentNode) { + div.remove(); + if (domCache.notification === div) domCache.notification = null; + } + }, NOTIFICATION_FADE_DURATION); } - }, 4000); + }, NOTIFICATION_DURATION); } -// 3. UI INJECTION (Robust) +// 4. UI INJECTION (Robust) function toggleSettingsModal() { if (!chrome.runtime?.id) { announceToScreenReader("Extension context invalid. Refresh page.", "red"); return; } + // Check cache first const existing = document.getElementById('webkeybind-shadow-root'); if (existing) { existing.remove(); + domCache.shadowRoot = null; announceToScreenReader("Settings window is closed", "red"); currentMode = null; updateHighlight(null); @@ -79,19 +98,21 @@ function toggleSettingsModal() { try { const host = document.createElement('div'); host.id = 'webkeybind-shadow-root'; - host.style.cssText = 'position: fixed; z-index: 2147483647; top: 0; left: 0; width: 0; height: 0;'; + host.style.cssText = `position: fixed; z-index: ${Z_INDEX_MAX}; top: 0; left: 0; width: 0; height: 0;`; document.body.appendChild(host); + domCache.shadowRoot = host; const shadow = host.attachShadow({ mode: 'open' }); const backdrop = document.createElement('div'); backdrop.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; - background: rgba(0,0,0,0.5); z-index: 2147483646; + background: rgba(0,0,0,0.5); z-index: ${Z_INDEX_MAX - 1}; backdrop-filter: blur(2px); `; backdrop.onclick = () => { host.remove(); + domCache.shadowRoot = null; announceToScreenReader("Settings window is closed", "red"); }; @@ -102,7 +123,7 @@ function toggleSettingsModal() { width: 900px; height: 650px; max-width: 95vw; max-height: 95vh; border: none; border-radius: 12px; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); - z-index: 2147483647; background: white; + z-index: ${Z_INDEX_MAX}; background: white; `; shadow.appendChild(backdrop); @@ -115,7 +136,7 @@ function toggleSettingsModal() { } } -// 4. CACHE SYSTEM +// 5. CACHE SYSTEM function updateShortcutCache() { if (!chrome?.storage?.local) return; try { @@ -136,7 +157,7 @@ try { }); } catch (e) { } -// 5. LISTENERS +// 6. LISTENERS function isInputActive() { const el = document.activeElement; if (!el) return false; @@ -221,7 +242,7 @@ document.addEventListener('click', (e) => { } }, true); -// 6. HIGHLIGHT ENGINE +// 7. HIGHLIGHT ENGINE function updateHighlight(newElement) { document.querySelectorAll('[data-webkeybind-highlight="true"]').forEach(el => { removeHighlight(el); @@ -252,7 +273,7 @@ function removeHighlight(el) { el.removeAttribute('data-webkeybind-highlight'); } -// 7. MODE SWITCHING +// 8. MODE SWITCHING function switchMode(newMode) { if (!chrome.runtime?.id) { announceToScreenReader("Please refresh the page.", "red"); return; } isLocked = false; @@ -295,7 +316,7 @@ function getClickableTarget(el) { return el.closest('button, a, input, select, textarea, [role="button"], [role="link"], [role="menuitem"], [role="checkbox"], [tabindex]:not([tabindex="-1"]), [class*="btn"], [data-testid], [aria-label]'); } -// 8. SAVE LOGIC +// 9. SAVE LOGIC function saveShortcut(element, key) { if (!chrome?.storage?.local) return; const currentHost = window.location.hostname; @@ -352,7 +373,7 @@ function saveShortcut(element, key) { }); } -// 9. EXECUTION LOGIC +// 10. EXECUTION LOGIC function runCachedShortcut(match) { let result = { element: null, healed: false }; @@ -380,6 +401,7 @@ function executeShortcut(element) { element.focus(); element.click(); } + function findElementWithHealing(profile) { if (!profile) return { element: null, healed: false }; if (profile.id && document.getElementById(profile.id)) { @@ -434,7 +456,14 @@ function generateRobustProfile(element) { path: generateCssPath(element) }; } -function findElementBySelector(selector) { try { return document.querySelector(selector); } catch { return null; } } + +function findElementBySelector(selector) { + try { + return document.querySelector(selector); + } catch { + return null; + } +} function generateCssPath(el) { if (!(el instanceof Element)) return; @@ -468,7 +497,7 @@ function generateCssPath(el) { return path.join(" > "); } -// 10. AUDIO READER +// 11. AUDIO READER function readAllShortcuts() { if (!chrome?.storage?.local) return; const currentHost = window.location.hostname; @@ -485,4 +514,4 @@ function readAllShortcuts() { announceToScreenReader(`Found ${siteShortcuts.length} shortcuts. ${spokenText}`); } }); -} \ No newline at end of file +} diff --git a/import_export.js b/import_export.js index 64d079e..b5d6a06 100644 --- a/import_export.js +++ b/import_export.js @@ -1,21 +1,38 @@ -document.addEventListener('DOMContentLoaded', () => { - const btnExportSite = document.getElementById('btn-export-site'); - const btnExportAll = document.getElementById('btn-export-all'); - const btnImport = document.getElementById('btn-import'); - - const menuContainer = document.querySelector('.menu-container'); - const menuBurger = document.querySelector('.menu-burger'); - const menuDropdown = document.querySelector('.import-export-dropdown'); - const closeMenuBtn = document.querySelector('.close-menu'); +// Import utilities +import { normalizeUrl } from './utils.js'; + +// DOM CACHE +const domCache = { + btnExportSite: null, + btnExportAll: null, + btnImport: null, + menuContainer: null, + menuBurger: null, + menuDropdown: null, + closeMenuBtn: null, + importModal: null, + dropZone: null, + fileInput: null, + btnCloseImport: null +}; - const importModal = document.getElementById('import-modal'); - const dropZone = document.getElementById('drop-zone'); - const fileInput = document.getElementById('file-input'); - const btnCloseImport = document.getElementById('btn-close-import'); +document.addEventListener('DOMContentLoaded', () => { + // Initialize DOM cache + domCache.btnExportSite = document.getElementById('btn-export-site'); + domCache.btnExportAll = document.getElementById('btn-export-all'); + domCache.btnImport = document.getElementById('btn-import'); + domCache.menuContainer = document.querySelector('.menu-container'); + domCache.menuBurger = document.querySelector('.menu-burger'); + domCache.menuDropdown = document.querySelector('.import-export-dropdown'); + domCache.closeMenuBtn = document.querySelector('.close-menu'); + domCache.importModal = document.getElementById('import-modal'); + domCache.dropZone = document.getElementById('drop-zone'); + domCache.fileInput = document.getElementById('file-input'); + domCache.btnCloseImport = document.getElementById('btn-close-import'); function handleFocusTrap(e) { if (e.key !== 'Tab') return; - const focusableElements = importModal.querySelectorAll( + const focusableElements = domCache.importModal.querySelectorAll( 'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])' ); @@ -36,34 +53,34 @@ document.addEventListener('DOMContentLoaded', () => { } } - if (menuBurger && menuDropdown) { - menuBurger.addEventListener('click', (e) => { + if (domCache.menuBurger && domCache.menuDropdown) { + domCache.menuBurger.addEventListener('click', (e) => { e.stopPropagation(); const langMenu = document.getElementById('lang-menu'); const langBtn = document.getElementById('lang-button'); if (langMenu) langMenu.style.display = 'none'; if (langBtn) langBtn.setAttribute('aria-expanded', 'false'); - const isVisible = menuDropdown.style.display === 'block'; - menuDropdown.style.display = isVisible ? 'none' : 'block'; + const isVisible = domCache.menuDropdown.style.display === 'block'; + domCache.menuDropdown.style.display = isVisible ? 'none' : 'block'; }); - if (menuContainer) { - menuContainer.addEventListener('focusout', (event) => { - if (!menuContainer.contains(event.relatedTarget)) { - menuDropdown.style.display = 'none'; + if (domCache.menuContainer) { + domCache.menuContainer.addEventListener('focusout', (event) => { + if (!domCache.menuContainer.contains(event.relatedTarget)) { + domCache.menuDropdown.style.display = 'none'; } }); } } - if (closeMenuBtn) { - closeMenuBtn.addEventListener('click', (e) => { + if (domCache.closeMenuBtn) { + domCache.closeMenuBtn.addEventListener('click', (e) => { e.stopPropagation(); - menuDropdown.style.display = 'none'; + domCache.menuDropdown.style.display = 'none'; }); } document.addEventListener('click', () => { - if (menuDropdown) menuDropdown.style.display = 'none'; + if (domCache.menuDropdown) domCache.menuDropdown.style.display = 'none'; }); function exportShortcuts(exportAll) { @@ -90,12 +107,12 @@ document.addEventListener('DOMContentLoaded', () => { }); } - if (btnExportSite) btnExportSite.addEventListener('click', () => exportShortcuts(false)); - if (btnExportAll) btnExportAll.addEventListener('click', () => exportShortcuts(true)); + if (domCache.btnExportSite) domCache.btnExportSite.addEventListener('click', () => exportShortcuts(false)); + if (domCache.btnExportAll) domCache.btnExportAll.addEventListener('click', () => exportShortcuts(true)); function openModal() { - importModal.style.display = 'flex'; - if (menuDropdown) menuDropdown.style.display = 'none'; + domCache.importModal.style.display = 'flex'; + if (domCache.menuDropdown) domCache.menuDropdown.style.display = 'none'; document.addEventListener('keydown', handleFocusTrap); const silentStart = document.getElementById('silent-start'); if (silentStart) { @@ -104,54 +121,54 @@ document.addEventListener('DOMContentLoaded', () => { } function closeModal() { - if (!importModal) return; - importModal.style.display = 'none'; + if (!domCache.importModal) return; + domCache.importModal.style.display = 'none'; document.removeEventListener('keydown', handleFocusTrap); - if (btnImport) btnImport.focus(); + if (domCache.btnImport) domCache.btnImport.focus(); } - if (btnImport) btnImport.addEventListener('click', openModal); - if (btnCloseImport) btnCloseImport.addEventListener('click', closeModal); + if (domCache.btnImport) domCache.btnImport.addEventListener('click', openModal); + if (domCache.btnCloseImport) domCache.btnCloseImport.addEventListener('click', closeModal); - if (importModal) { - importModal.addEventListener('click', (e) => { - if (e.target === importModal) closeModal(); + if (domCache.importModal) { + domCache.importModal.addEventListener('click', (e) => { + if (e.target === domCache.importModal) closeModal(); }); document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && importModal.style.display === 'flex') { + if (e.key === 'Escape' && domCache.importModal.style.display === 'flex') { closeModal(); } }); } - if (dropZone) { - dropZone.addEventListener('click', () => fileInput.click()); - dropZone.addEventListener('keydown', (e) => { + if (domCache.dropZone) { + domCache.dropZone.addEventListener('click', () => domCache.fileInput.click()); + domCache.dropZone.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); - fileInput.click(); + domCache.fileInput.click(); } }); - dropZone.addEventListener('dragover', (e) => { + domCache.dropZone.addEventListener('dragover', (e) => { e.preventDefault(); - dropZone.style.backgroundColor = '#f0ebff'; - dropZone.style.borderColor = '#7c4dff'; + domCache.dropZone.style.backgroundColor = '#f0ebff'; + domCache.dropZone.style.borderColor = '#7c4dff'; }); - dropZone.addEventListener('dragleave', () => { - dropZone.style.backgroundColor = '#f9f9f9'; - dropZone.style.borderColor = '#ccc'; + domCache.dropZone.addEventListener('dragleave', () => { + domCache.dropZone.style.backgroundColor = '#f9f9f9'; + domCache.dropZone.style.borderColor = '#ccc'; }); - dropZone.addEventListener('drop', (e) => { + domCache.dropZone.addEventListener('drop', (e) => { e.preventDefault(); - dropZone.style.backgroundColor = '#f9f9f9'; + domCache.dropZone.style.backgroundColor = '#f9f9f9'; if (e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]); }); } - if (fileInput) { - fileInput.addEventListener('change', (e) => { + if (domCache.fileInput) { + domCache.fileInput.addEventListener('change', (e) => { if (e.target.files.length) processFile(e.target.files[0]); - fileInput.value = ''; + domCache.fileInput.value = ''; }); } diff --git a/index.html b/index.html index 82132ac..8f0c724 100644 --- a/index.html +++ b/index.html @@ -106,8 +106,8 @@