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 @@ - - + + \ No newline at end of file diff --git a/manifest.json b/manifest.json index adf351d..b678427 100644 --- a/manifest.json +++ b/manifest.json @@ -37,9 +37,11 @@ "" ], "js": [ - "language.js","content.js" + "language.js", + "content.js" ], - "run_at": "document_idle" + "run_at": "document_idle", + "type": "module" } ], "web_accessible_resources": [ @@ -47,6 +49,7 @@ "resources": [ "index.html", "style.css", + "utils.js", "popup.js", "language.js", "import_export.js" diff --git a/popup.js b/popup.js index 197d127..22cfa3d 100644 --- a/popup.js +++ b/popup.js @@ -1,39 +1,43 @@ -// VALIDATION & HELPER UTILITIES -function isValidURL(string) { - if (!string) return false; - try { - new URL(string); - return true; - } catch (_) { - try { - new URL('https://' + string); - return true; - } catch (__) { - return false; - } - } -} - -function normalizeUrl(url) { - return url.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").split('/')[0].toLowerCase(); -} +// Import utilities +import { + isValidURL, + normalizeUrl, + createAnnouncer, + announceToScreenReader as announce, + debounce, + DEBOUNCE_DELAY +} from './utils.js'; + +// DOM CACHE +const domCache = { + announcer: null, + shortcutList: null, + addBtn: null, + showAllBtn: null, + deleteAllBtn: null, + alertElement: null +}; // INITIALIZATION & MAIN LOGIC document.addEventListener('DOMContentLoaded', () => { window.currentLang = "English"; - const popupAnnouncer = document.createElement('div'); - popupAnnouncer.setAttribute('aria-live', 'assertive'); - popupAnnouncer.setAttribute('aria-atomic', 'true'); - popupAnnouncer.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;'; - document.body.appendChild(popupAnnouncer); + // Initialize DOM cache + domCache.announcer = createAnnouncer('webkeybind-popup-announcer'); + document.body.appendChild(domCache.announcer); + + domCache.shortcutList = document.querySelector('.shortcut-list'); + domCache.addBtn = document.querySelector('.btn-add'); + domCache.showAllBtn = document.querySelector('.btn-show-all'); + domCache.deleteAllBtn = document.querySelector('.btn-delete-all') || document.getElementById('btn-delete-all'); function showAccessibleAlert(msg, type = "error") { - popupAnnouncer.textContent = ''; - setTimeout(() => { popupAnnouncer.textContent = msg; }, 50); + announce(domCache.announcer, msg); - const existing = document.getElementById('webkeybind-popup-alert'); - if (existing) existing.remove(); + // Remove existing alert using cache + if (domCache.alertElement && domCache.alertElement.parentNode) { + domCache.alertElement.remove(); + } const alertDiv = document.createElement('div'); alertDiv.id = 'webkeybind-popup-alert'; @@ -53,6 +57,7 @@ document.addEventListener('DOMContentLoaded', () => { animation: popup-fadein 0.3s ease-out; `; document.body.appendChild(alertDiv); + domCache.alertElement = alertDiv; if (!document.getElementById('popup-alert-styles')) { const style = document.createElement('style'); @@ -65,7 +70,14 @@ document.addEventListener('DOMContentLoaded', () => { if (document.body.contains(alertDiv)) { alertDiv.style.opacity = "0"; alertDiv.style.transition = "opacity 0.3s"; - setTimeout(() => { if (document.body.contains(alertDiv)) alertDiv.remove(); }, 300); + setTimeout(() => { + if (document.body.contains(alertDiv)) { + alertDiv.remove(); + if (domCache.alertElement === alertDiv) { + domCache.alertElement = null; + } + } + }, 300); } }, 3000); } @@ -73,8 +85,7 @@ document.addEventListener('DOMContentLoaded', () => { function showAccessibleConfirm(msg, onConfirmCallback) { const t = window.translations?.[window.currentLang] || window.translations?.['English'] || {}; - popupAnnouncer.textContent = ''; - setTimeout(() => { popupAnnouncer.textContent = msg + " Press Tab to select options."; }, 50); + announce(domCache.announcer, msg + " Press Tab to select options."); const existing = document.getElementById('wkb-confirm-modal'); if (existing) existing.remove(); @@ -143,11 +154,6 @@ document.addEventListener('DOMContentLoaded', () => { btnCancel.focus(); } - const shortcutList = document.querySelector('.shortcut-list'); - const addBtn = document.querySelector('.btn-add'); - const showAllBtn = document.querySelector('.btn-show-all'); - const deleteAllBtn = document.querySelector('.btn-delete-all') || document.getElementById('btn-delete-all'); - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (tabs[0]?.id) chrome.tabs.connect(tabs[0].id, { name: "z-webkeybind-popup" }); }); @@ -169,11 +175,11 @@ document.addEventListener('DOMContentLoaded', () => { }); window.loadShortcuts = function () { - shortcutList.innerHTML = ''; + domCache.shortcutList.innerHTML = ''; const t = window.translations?.[window.currentLang] || window.translations?.['English'] || {}; - if (showAllBtn) { - showAllBtn.innerHTML = `${isShowingAll ? (t.showCurrent || "Show Current") : (t.showAll || "Show All")} ${isShowingAll ? '⌃' : '⌄'}`; + if (domCache.showAllBtn) { + domCache.showAllBtn.innerHTML = `${isShowingAll ? (t.showCurrent || "Show Current") : (t.showAll || "Show All")} ${isShowingAll ? '⌃' : '⌄'}`; } chrome.storage.local.get(null, (items) => { @@ -190,13 +196,23 @@ document.addEventListener('DOMContentLoaded', () => { const msg = document.createElement('div'); msg.style.cssText = "text-align:center; padding:20px; color:#999; font-size:13px; font-style:italic;"; msg.innerText = `${t.no_shortcuts || "No shortcuts"} ${isShowingAll ? '' : window.currentSiteHostname}`; - shortcutList.appendChild(msg); + domCache.shortcutList.appendChild(msg); } else { displayList.forEach((data, index) => createRow(data, index + 1)); } }); }; + // Debounced save function + const debouncedSave = debounce((entry) => { + if (entry.url && isValidURL(entry.url) && entry.name && entry.name.trim() !== "" && + entry.elementId && entry.elementId.trim() !== "" && entry.key && entry.key.trim() !== "") { + chrome.storage.local.set({ [`shortcut_${entry.id}`]: entry }); + } else { + chrome.storage.local.remove(`shortcut_${entry.id}`); + } + }, DEBOUNCE_DELAY); + function createRow(data, index) { const t = window.translations?.[window.currentLang] || window.translations?.['English'] || {}; const row = document.createElement('div'); @@ -249,11 +265,11 @@ document.addEventListener('DOMContentLoaded', () => { return; } data[field] = value; - validateAndSave(data); + debouncedSave(data); }); } else if (field !== 'elementId') { data[field] = value; - validateAndSave(data); + debouncedSave(data); } }); }); @@ -264,7 +280,7 @@ document.addEventListener('DOMContentLoaded', () => { const val = e.target.value.trim(); if (val === "") { data.elementId = ""; - validateAndSave(data); + debouncedSave(data); return; } chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { @@ -292,21 +308,13 @@ document.addEventListener('DOMContentLoaded', () => { } else { e.target.classList.remove('input-error'); data.elementId = val; - validateAndSave(data); + debouncedSave(data); } }); }); }); } - function validateAndSave(entry) { - if (entry.url && isValidURL(entry.url) && entry.name && entry.name.trim() !== "" && entry.elementId && entry.elementId.trim() !== "" && entry.key && entry.key.trim() !== "") { - chrome.storage.local.set({ [`shortcut_${entry.id}`]: entry }); - } else { - chrome.storage.local.remove(`shortcut_${entry.id}`); - } - } - row.querySelector('.btn-remove').addEventListener('click', () => { showAccessibleConfirm(t.delete_confirm || "Delete this shortcut?", () => { chrome.storage.local.remove(`shortcut_${data.id}`, () => { @@ -317,28 +325,28 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - shortcutList.appendChild(row); + domCache.shortcutList.appendChild(row); } - if (showAllBtn) { - showAllBtn.addEventListener('click', () => { + if (domCache.showAllBtn) { + domCache.showAllBtn.addEventListener('click', () => { isShowingAll = !isShowingAll; window.loadShortcuts(); }); } - if (addBtn) { - addBtn.addEventListener('click', () => { + if (domCache.addBtn) { + domCache.addBtn.addEventListener('click', () => { const newShortcut = { id: Date.now().toString(), url: window.currentSiteHostname || "example.com", name: "", elementId: "", key: "" }; createRow(newShortcut, document.querySelectorAll('.shortcut-row').length + 1); - shortcutList.scrollTop = shortcutList.scrollHeight; + domCache.shortcutList.scrollTop = domCache.shortcutList.scrollHeight; }); } document.querySelectorAll('.language-dropdown, .menu-container').forEach(el => el.removeAttribute('tabindex')); - if (deleteAllBtn) { - deleteAllBtn.addEventListener('click', () => { + if (domCache.deleteAllBtn) { + domCache.deleteAllBtn.addEventListener('click', () => { const t = window.translations?.[window.currentLang] || window.translations?.['English'] || {}; const host = window.currentSiteHostname || ""; showAccessibleConfirm(t.delete_all_confirm || "Delete these shortcuts?", () => { @@ -356,4 +364,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }); } -}); \ No newline at end of file +}); diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..57e04da --- /dev/null +++ b/utils.js @@ -0,0 +1,123 @@ +// Shared utility functions for WebKeyBind extension + +/** + * Validates if a string is a valid URL + * @param {string} string - The string to validate + * @returns {boolean} True if valid URL + */ +export function isValidURL(string) { + if (!string) return false; + try { + new URL(string); + return true; + } catch (_) { + try { + new URL('https://' + string); + return true; + } catch (__) { + return false; + } + } +} + +/** + * Normalizes a URL by removing protocol and www prefix + * @param {string} url - The URL to normalize + * @returns {string} Normalized URL hostname + */ +export function normalizeUrl(url) { + return url.replace(/^(?:https?:\/\/)?(?:www\.)?/i, "").split('/')[0].toLowerCase(); +} + +/** + * Creates an accessible screen reader announcer element + * @param {string} id - The ID for the announcer element + * @returns {HTMLElement} The announcer element + */ +export function createAnnouncer(id = 'webkeybind-announcer') { + const announcer = document.createElement('div'); + announcer.id = id; + announcer.setAttribute('aria-live', 'assertive'); + announcer.setAttribute('aria-atomic', 'true'); + announcer.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;'; + return announcer; +} + +/** + * Announces a message to screen readers + * @param {HTMLElement} announcer - The announcer element + * @param {string} message - The message to announce + * @param {string} langCode - ISO language code (e.g., 'en', 'hi') + */ +export function announceToScreenReader(announcer, message, langCode = 'en') { + if (!announcer) return; + announcer.setAttribute('lang', langCode); + announcer.textContent = ''; + setTimeout(() => { announcer.textContent = message; }, 50); +} + +/** + * Debounces a function call + * @param {Function} func - The function to debounce + * @param {number} wait - The delay in milliseconds + * @returns {Function} Debounced function + */ +export function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +/** + * Removes an element by ID if it exists + * @param {string} id - The element ID to remove + */ +export function removeElementById(id) { + const el = document.getElementById(id); + if (el) el.remove(); +} + +/** + * Language code mapping + */ +export const LANG_MAP = { + "English": "en", + "हिंदी": "hi", + "मराठी": "mr", + "മലയാളം": "ml" +}; + +/** + * Gets ISO language code from language name + * @param {string} langName - Language name + * @returns {string} ISO language code + */ +export function getISOCode(langName) { + return LANG_MAP[langName] || "en"; +} + +/** + * Notification color constants + */ +export const NOTIFICATION_COLORS = { + default: '#333333', + blue: '#007BFF', + purple: '#6f42c1', + orange: '#FF9800', + red: '#DC3545', + green: '#28A745' +}; + +/** + * Other constants + */ +export const Z_INDEX_MAX = 2147483647; +export const NOTIFICATION_DURATION = 4000; +export const NOTIFICATION_FADE_DURATION = 300; +export const DEBOUNCE_DELAY = 300;