z.WebKeyBind
+163
-73
@@ -21,2 +21,5 @@ document.addEventListener('DOMContentLoaded', () => { | ||
| // --- CHECK IF WE ARE IN THE "BROWSE ONLY" FULL TAB --- | ||
| const isFullTabMode = new URLSearchParams(window.location.search).get('importMode') === 'true'; | ||
| // --- 1. FOCUS TRAP LOGIC --- | ||
@@ -102,3 +105,3 @@ function handleFocusTrap(e) { | ||
| // --- 4. IMPORT LOGIC --- | ||
| // --- 4. THE FULL TAB IMPORT TRICK --- | ||
| function openModal() { | ||
@@ -109,3 +112,3 @@ importModal.style.display = 'flex'; | ||
| setTimeout(() => { document.getElementById('silent-start')?.focus(); }, 50); | ||
| if(window.showAccessibleAlert) window.showAccessibleAlert("Import menu opened. Drop or select a JSON file.", "info"); | ||
| if(window.showAccessibleAlert) window.showAccessibleAlert("Import menu opened. Select a JSON file.", "info"); | ||
| } | ||
@@ -117,7 +120,26 @@ | ||
| document.removeEventListener('keydown', handleFocusTrap); | ||
| if (btnImport) btnImport.focus(); | ||
| if(window.showAccessibleAlert) window.showAccessibleAlert("Import menu closed.", "info"); | ||
| if (window.showAccessibleAlert) window.showAccessibleAlert("Import menu closed.", "info"); | ||
| // If in the full tab, closing the modal should close the entire tab | ||
| if (isFullTabMode) { | ||
| window.close(); | ||
| } else if (btnImport) { | ||
| btnImport.focus(); | ||
| } | ||
| } | ||
| if (btnImport) btnImport.addEventListener('click', openModal); | ||
| if (btnImport) { | ||
| btnImport.addEventListener('click', () => { | ||
| if (isFullTabMode) { | ||
| openModal(); | ||
| } else { | ||
| // Open THIS file in a new tab to bypass Firefox security | ||
| const currentHtmlFile = window.location.pathname; | ||
| chrome.tabs.create({ url: currentHtmlFile + "?importMode=true" }); | ||
| window.close(); // Close the tiny popup | ||
| } | ||
| }); | ||
| } | ||
| if (btnCloseImport) btnCloseImport.addEventListener('click', closeModal); | ||
@@ -130,13 +152,39 @@ | ||
| // --- 5. DRAG & DROP HANDLING --- | ||
| // --- ISOLATE THE BROWSE BOX IN FULL TAB MODE --- | ||
| if (isFullTabMode) { | ||
| // 1. Move the import modal to the very top level of the body | ||
| document.body.appendChild(importModal); | ||
| // 2. Hide everything else in the extension so it looks like a clean, dedicated page | ||
| Array.from(document.body.children).forEach(child => { | ||
| if (child !== importModal && child.tagName !== 'SCRIPT') { | ||
| child.style.display = 'none'; | ||
| } | ||
| }); | ||
| // 3. Make the modal fill the screen with a clean background | ||
| importModal.style.display = 'flex'; | ||
| importModal.style.position = 'fixed'; | ||
| importModal.style.top = '0'; | ||
| importModal.style.left = '0'; | ||
| importModal.style.width = '100vw'; | ||
| importModal.style.height = '100vh'; | ||
| importModal.style.backgroundColor = '#f8f9fa'; // Clean light grey background | ||
| importModal.style.zIndex = '999999'; | ||
| // 4. Hide the "Close" button since they can just close the browser tab to cancel | ||
| if (btnCloseImport) btnCloseImport.style.display = 'none'; | ||
| // Trigger the focus trap for accessibility | ||
| setTimeout(openModal, 100); | ||
| } | ||
| // --- 5. STANDARD FILE CLICK --- | ||
| if (dropZone) { | ||
| dropZone.addEventListener('click', () => fileInput.click()); | ||
| dropZone.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); fileInput.click(); } }); | ||
| dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.backgroundColor = '#f0ebff'; }); | ||
| dropZone.addEventListener('dragleave', () => { dropZone.style.backgroundColor = '#f9f9f9'; }); | ||
| dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.backgroundColor = '#f9f9f9'; if (e.dataTransfer.files.length) processFile(e.dataTransfer.files[0]); }); | ||
| } | ||
| if (fileInput) fileInput.addEventListener('change', (e) => { if (e.target.files.length) processFile(e.target.files[0]); fileInput.value = ''; }); | ||
| // --- NEW: 4-BUTTON ACCESSIBLE CONFLICT MODAL WITH CANCEL BEHAVIOR --- | ||
| // --- ACCESSIBLE CONFLICT MODAL --- | ||
| function showConflictResolutionModal(msg, onReplace, onReplaceAll, onSkip, onSkipAll, onCancel) { | ||
@@ -162,3 +210,2 @@ let popupAnnouncer = document.getElementById('wkb-conflict-announcer'); | ||
| modal.setAttribute('aria-modal', 'true'); | ||
| // FIX: Applied modal-fadein here | ||
| modal.style.cssText = `position: relative; background: white; padding: 24px; border-radius: 8px; width: 380px; max-width: 90%; box-shadow: 0 10px 25px rgba(0,0,0,0.2); text-align: center; font-family: sans-serif; animation: modal-fadein 0.2s ease-out; display: flex; flex-direction: column; gap: 16px;`; | ||
@@ -225,3 +272,3 @@ | ||
| // --- 6. FILE PROCESSOR --- | ||
| // --- 6. FILE PROCESSOR (FIREFOX SAFE BATCH SAVE) --- | ||
| function processFile(file) { | ||
@@ -234,79 +281,122 @@ if (!file.name.endsWith('.json')) { | ||
| reader.onload = (e) => { | ||
| let importedData; | ||
| try { | ||
| const importedData = JSON.parse(e.target.result); | ||
| if (!Array.isArray(importedData)) throw new Error(); | ||
| importedData = JSON.parse(e.target.result); | ||
| if (!Array.isArray(importedData)) throw new Error("Not array"); | ||
| } catch (err) { | ||
| if(window.showAccessibleAlert) window.showAccessibleAlert("Invalid JSON file format.", "error"); | ||
| return; | ||
| } | ||
| chrome.storage.local.get(null, (existingItems) => { | ||
| const existingShortcuts = Object.values(existingItems).filter(item => item.id && item.key); | ||
| let importCount = 0, skipCount = 0, replaceCount = 0; | ||
| chrome.storage.local.get(null, (existingItems) => { | ||
| const existingShortcuts = Object.values(existingItems).filter(item => item.id && item.key); | ||
| let importCount = 0, skipCount = 0, replaceCount = 0; | ||
| let globalConflictAction = null; | ||
| let pendingSaves = {}; | ||
| let keysToRemove = []; | ||
| const finalizeBatchSave = () => { | ||
| importModal.style.display = 'none'; | ||
| document.removeEventListener('keydown', handleFocusTrap); | ||
| let globalConflictAction = null; | ||
| const handleCancel = () => { | ||
| importModal.style.display = 'none'; | ||
| document.removeEventListener('keydown', handleFocusTrap); | ||
| if (window.showAccessibleAlert) window.showAccessibleAlert(`Import Cancelled. Added: ${importCount}, Replaced: ${replaceCount}, Skipped: ${skipCount}`, "info"); | ||
| const finishUp = () => { | ||
| if (window.showAccessibleAlert) window.showAccessibleAlert(`Import Complete! Added: ${importCount}, Replaced: ${replaceCount}, Skipped: ${skipCount}`, "success"); | ||
| if (window.loadShortcuts) window.loadShortcuts(); | ||
| if (btnImport) btnImport.focus(); | ||
| // Wait 1.5 seconds so they can see/hear the success alert, then close the tab! | ||
| if (isFullTabMode) { | ||
| setTimeout(() => { window.close(); }, 1500); | ||
| } else if (btnImport) { | ||
| btnImport.focus(); | ||
| } | ||
| }; | ||
| const processItem = (index) => { | ||
| if (index >= importedData.length) { | ||
| importModal.style.display = 'none'; document.removeEventListener('keydown', handleFocusTrap); | ||
| if (window.showAccessibleAlert) window.showAccessibleAlert(`Import Complete! Added: ${importCount}, Replaced: ${replaceCount}, Skipped: ${skipCount}`, "success"); | ||
| if (window.loadShortcuts) window.loadShortcuts(); | ||
| if (btnImport) btnImport.focus(); | ||
| return; | ||
| } | ||
| if (keysToRemove.length > 0) { | ||
| chrome.storage.local.remove(keysToRemove, () => { | ||
| if (Object.keys(pendingSaves).length > 0) chrome.storage.local.set(pendingSaves, finishUp); | ||
| else finishUp(); | ||
| }); | ||
| } else if (Object.keys(pendingSaves).length > 0) { | ||
| chrome.storage.local.set(pendingSaves, finishUp); | ||
| } else { | ||
| finishUp(); | ||
| } | ||
| }; | ||
| const item = importedData[index]; | ||
| if (!item.id || !item.key || !item.url) { processItem(index + 1); return; } | ||
| const handleCancel = () => { | ||
| importModal.style.display = 'none'; | ||
| document.removeEventListener('keydown', handleFocusTrap); | ||
| if (window.showAccessibleAlert) window.showAccessibleAlert(`Import Cancelled. Added: ${importCount}, Replaced: ${replaceCount}, Skipped: ${skipCount}`, "info"); | ||
| if (window.loadShortcuts) window.loadShortcuts(); | ||
| if (isFullTabMode) { | ||
| window.close(); | ||
| } else if (btnImport) { | ||
| btnImport.focus(); | ||
| } | ||
| }; | ||
| const normImpUrl = getNormUrl(item.url); | ||
| const conflict = existingShortcuts.find(ex => ex.key === item.key && (getNormUrl(ex.url) === normImpUrl || ex.url === "<URL>" || item.url === "<URL>")); | ||
| const processItem = (index) => { | ||
| if (index >= importedData.length) { | ||
| finalizeBatchSave(); | ||
| return; | ||
| } | ||
| const saveItem = (isReplace) => { | ||
| chrome.storage.local.set({ [`shortcut_${item.id}`]: item }, () => { | ||
| if (isReplace) replaceCount++; else importCount++; | ||
| processItem(index + 1); | ||
| }); | ||
| }; | ||
| const item = importedData[index]; | ||
| if (!item.id || !item.key || !item.url) { processItem(index + 1); return; } | ||
| const handleReplace = () => { | ||
| chrome.storage.local.remove(`shortcut_${conflict.id}`, () => { | ||
| const cIndex = existingShortcuts.findIndex(e => e.id === conflict.id); | ||
| if (cIndex > -1) existingShortcuts.splice(cIndex, 1); | ||
| existingShortcuts.push(item); saveItem(true); | ||
| }); | ||
| }; | ||
| const normImpUrl = getNormUrl(item.url); | ||
| const conflict = existingShortcuts.find(ex => ex.key === item.key && (getNormUrl(ex.url) === normImpUrl || ex.url === "<URL>" || item.url === "<URL>")); | ||
| const handleSkip = () => { skipCount++; processItem(index + 1); }; | ||
| const handleReplace = () => { | ||
| keysToRemove.push(`shortcut_${conflict.id}`); | ||
| const cIndex = existingShortcuts.findIndex(e => e.id === conflict.id); | ||
| if (cIndex > -1) existingShortcuts.splice(cIndex, 1); | ||
| // Guarantee new unique ID | ||
| const newId = Date.now().toString() + Math.random().toString(36).substring(2, 6); | ||
| item.id = newId; | ||
| existingShortcuts.push(item); | ||
| pendingSaves[`shortcut_${newId}`] = item; | ||
| replaceCount++; | ||
| processItem(index + 1); | ||
| }; | ||
| if (conflict) { | ||
| if (globalConflictAction === 'replace_all') { | ||
| handleReplace(); | ||
| } else if (globalConflictAction === 'skip_all') { | ||
| handleSkip(); | ||
| } else { | ||
| const msg = `Conflict! Key '${item.key}' is already used for '${conflict.name}'. What would you like to do?`; | ||
| showConflictResolutionModal( | ||
| msg, | ||
| () => { handleReplace(); }, | ||
| () => { globalConflictAction = 'replace_all'; handleReplace(); }, | ||
| () => { handleSkip(); }, | ||
| () => { globalConflictAction = 'skip_all'; handleSkip(); }, | ||
| () => { handleCancel(); } | ||
| ); | ||
| } | ||
| } else { | ||
| existingShortcuts.push(item); | ||
| saveItem(false); | ||
| const handleSkip = () => { skipCount++; processItem(index + 1); }; | ||
| if (conflict) { | ||
| if (globalConflictAction === 'replace_all') { | ||
| handleReplace(); | ||
| } else if (globalConflictAction === 'skip_all') { | ||
| handleSkip(); | ||
| } else { | ||
| const msg = `Conflict! Key '${item.key}' is already used for '${conflict.name}'. What would you like to do?`; | ||
| showConflictResolutionModal( | ||
| msg, | ||
| () => { handleReplace(); }, | ||
| () => { globalConflictAction = 'replace_all'; handleReplace(); }, | ||
| () => { handleSkip(); }, | ||
| () => { globalConflictAction = 'skip_all'; handleSkip(); }, | ||
| () => { handleCancel(); } | ||
| ); | ||
| } | ||
| }; | ||
| processItem(0); | ||
| }); | ||
| } catch (err) { if(window.showAccessibleAlert) window.showAccessibleAlert("Invalid JSON file format.", "error"); } | ||
| } else { | ||
| const newId = Date.now().toString() + Math.random().toString(36).substring(2, 6); | ||
| item.id = newId; | ||
| existingShortcuts.push(item); | ||
| pendingSaves[`shortcut_${newId}`] = item; | ||
| importCount++; | ||
| processItem(index + 1); | ||
| } | ||
| }; | ||
| processItem(0); | ||
| }); | ||
| }; | ||
| reader.readAsText(file); | ||
| } | ||
| }); |
+1
-1
| { | ||
| "manifest_version": 3, | ||
| "name": "z.WebKeyBind", | ||
| "version": "1.0.1", | ||
| "version": "1.0.2", | ||
| "description": "Use Custom Shortcut key to Trigger the button of the page. ", | ||
@@ -6,0 +6,0 @@ "permissions": [ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet