feat: confirm removal dialogs with Enter
Make selected and all-item removal dialogs focus their destructive confirmation action so Enter confirms immediately. Preserve cancel-first behavior for every other dangerous dialog and keep Tab, Escape, alternate, close, overlay, and focus restoration semantics safe.
This commit is contained in:
+23
-8
@@ -2497,7 +2497,7 @@ function hideContextMenu() {
|
|||||||
async function deleteSelectedRecentFiles() {
|
async function deleteSelectedRecentFiles() {
|
||||||
if (selectedRecentIds.size === 0) return;
|
if (selectedRecentIds.size === 0) return;
|
||||||
const count = selectedRecentIds.size;
|
const count = selectedRecentIds.size;
|
||||||
if (!await showAppConfirm({ title: 'Ausgewählte Einträge entfernen?', message: count === 1 ? 'Ein ausgewählter Eintrag wird aus diesem Panel entfernt.' : `${count} ausgewählte Einträge werden aus diesem Panel entfernt.`, confirmText: 'Entfernen', danger: true })) return;
|
if (!await showAppConfirm({ title: 'Ausgewählte Einträge entfernen?', message: count === 1 ? 'Ein ausgewählter Eintrag wird aus diesem Panel entfernt.' : `${count} ausgewählte Einträge werden aus diesem Panel entfernt.`, confirmText: 'Entfernen', danger: true, defaultAction: 'confirm' })) return;
|
||||||
let removedDone = 0, removedErr = 0;
|
let removedDone = 0, removedErr = 0;
|
||||||
sessionFilesData = sessionFilesData.filter(r => {
|
sessionFilesData = sessionFilesData.filter(r => {
|
||||||
if (!selectedRecentIds.has(r.order)) return true;
|
if (!selectedRecentIds.has(r.order)) return true;
|
||||||
@@ -2514,7 +2514,7 @@ async function deleteSelectedRecentFiles() {
|
|||||||
|
|
||||||
async function clearAllRecentFiles() {
|
async function clearAllRecentFiles() {
|
||||||
if (sessionFilesData.length === 0) return;
|
if (sessionFilesData.length === 0) return;
|
||||||
if (!await showAppConfirm({ title: 'Alle Links entfernen?', message: `Wirklich alle ${sessionFilesData.length} Links aus diesem Panel entfernen?`, confirmText: 'Alle entfernen', danger: true })) return;
|
if (!await showAppConfirm({ title: 'Alle Links entfernen?', message: `Wirklich alle ${sessionFilesData.length} Links aus diesem Panel entfernen?`, confirmText: 'Alle entfernen', danger: true, defaultAction: 'confirm' })) return;
|
||||||
sessionFilesData = [];
|
sessionFilesData = [];
|
||||||
_sessionFileKeys.clear();
|
_sessionFileKeys.clear();
|
||||||
_sessionDoneCount = 0;
|
_sessionDoneCount = 0;
|
||||||
@@ -3215,7 +3215,7 @@ async function handleContextAction(action, targetJobId = null) {
|
|||||||
await copyFailureDetails(targetJobId);
|
await copyFailureDetails(targetJobId);
|
||||||
} else if (action === 'delete-selected') {
|
} else if (action === 'delete-selected') {
|
||||||
const count = selectedJobIds.size;
|
const count = selectedJobIds.size;
|
||||||
if (!count || !await showAppConfirm({ title: 'Uploads entfernen?', message: count === 1 ? 'Ein ausgewählter Upload wird aus der Liste entfernt.' : `${count} ausgewählte Uploads werden aus der Liste entfernt.`, confirmText: 'Entfernen', danger: true })) return;
|
if (!count || !await showAppConfirm({ title: 'Uploads entfernen?', message: count === 1 ? 'Ein ausgewählter Upload wird aus der Liste entfernt.' : `${count} ausgewählte Uploads werden aus der Liste entfernt.`, confirmText: 'Entfernen', danger: true, defaultAction: 'confirm' })) return;
|
||||||
const activeIds = [...selectedJobIds].filter(id => {
|
const activeIds = [...selectedJobIds].filter(id => {
|
||||||
const j = _jobIndexById.get(id);
|
const j = _jobIndexById.get(id);
|
||||||
return j && (j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server');
|
return j && (j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server');
|
||||||
@@ -3241,7 +3241,7 @@ async function handleContextAction(action, targetJobId = null) {
|
|||||||
} else if (action === 'copy-all-links') {
|
} else if (action === 'copy-all-links') {
|
||||||
copyAllLinks();
|
copyAllLinks();
|
||||||
} else if (action === 'delete-all') {
|
} else if (action === 'delete-all') {
|
||||||
if (!queueJobs.length || !await showAppConfirm({ title: 'Alle Uploads entfernen?', message: `${queueJobs.length} ${queueJobs.length === 1 ? 'Upload wird' : 'Uploads werden'} aus der Liste entfernt.`, confirmText: 'Alle entfernen', danger: true })) return;
|
if (!queueJobs.length || !await showAppConfirm({ title: 'Alle Uploads entfernen?', message: `${queueJobs.length} ${queueJobs.length === 1 ? 'Upload wird' : 'Uploads werden'} aus der Liste entfernt.`, confirmText: 'Alle entfernen', danger: true, defaultAction: 'confirm' })) return;
|
||||||
const hasActiveJobs = uploading || queueJobs.some(j => j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server');
|
const hasActiveJobs = uploading || queueJobs.some(j => j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server');
|
||||||
if (hasActiveJobs) {
|
if (hasActiveJobs) {
|
||||||
_cancelAutoRetry(true);
|
_cancelAutoRetry(true);
|
||||||
@@ -4719,6 +4719,7 @@ function closeAppAlert(result = false) {
|
|||||||
if (!modal) return;
|
if (!modal) return;
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
modal.setAttribute('aria-hidden', 'true');
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
delete modal.dataset.defaultAction;
|
||||||
_setAppAlertBackgroundInert(false);
|
_setAppAlertBackgroundInert(false);
|
||||||
const resolve = _appAlertResolve;
|
const resolve = _appAlertResolve;
|
||||||
_appAlertResolve = null;
|
_appAlertResolve = null;
|
||||||
@@ -4728,7 +4729,7 @@ function closeAppAlert(result = false) {
|
|||||||
if (resolve) resolve(result);
|
if (resolve) resolve(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAppDialog({ message, title = 'Hinweis', confirmText = 'OK', cancelText = 'Abbrechen', alternateText = '', showCancel = false, danger = false } = {}) {
|
function showAppDialog({ message, title = 'Hinweis', confirmText = 'OK', cancelText = 'Abbrechen', alternateText = '', showCancel = false, danger = false, defaultAction = 'cancel' } = {}) {
|
||||||
const modal = document.getElementById('appAlertModal');
|
const modal = document.getElementById('appAlertModal');
|
||||||
const titleEl = document.getElementById('appAlertTitle');
|
const titleEl = document.getElementById('appAlertTitle');
|
||||||
const messageEl = document.getElementById('appAlertMessage');
|
const messageEl = document.getElementById('appAlertMessage');
|
||||||
@@ -4748,8 +4749,9 @@ function showAppDialog({ message, title = 'Hinweis', confirmText = 'OK', cancelT
|
|||||||
alternate.hidden = !alternateText;
|
alternate.hidden = !alternateText;
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
modal.setAttribute('aria-hidden', 'false');
|
modal.setAttribute('aria-hidden', 'false');
|
||||||
|
modal.dataset.defaultAction = defaultAction === 'confirm' ? 'confirm' : 'cancel';
|
||||||
_setAppAlertBackgroundInert(true);
|
_setAppAlertBackgroundInert(true);
|
||||||
(showCancel ? cancel : confirm).focus();
|
(showCancel && modal.dataset.defaultAction !== 'confirm' ? cancel : confirm).focus();
|
||||||
return new Promise(resolve => { _appAlertResolve = resolve; });
|
return new Promise(resolve => { _appAlertResolve = resolve; });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4757,8 +4759,8 @@ function showAppAlert(message, title = 'Hinweis') {
|
|||||||
return showAppDialog({ message, title });
|
return showAppDialog({ message, title });
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAppConfirm({ message, title = 'Bestätigen', confirmText = 'Bestätigen', cancelText = 'Abbrechen', danger = false } = {}) {
|
function showAppConfirm({ message, title = 'Bestätigen', confirmText = 'Bestätigen', cancelText = 'Abbrechen', danger = false, defaultAction = 'cancel' } = {}) {
|
||||||
return showAppDialog({ message, title, confirmText, cancelText, showCancel: true, danger });
|
return showAppDialog({ message, title, confirmText, cancelText, showCancel: true, danger, defaultAction });
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAppChoice({ message, title, confirmText, alternateText, cancelText = 'Abbrechen' } = {}) {
|
function showAppChoice({ message, title, confirmText, alternateText, cancelText = 'Abbrechen' } = {}) {
|
||||||
@@ -7686,6 +7688,19 @@ function setupListeners() {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
const confirm = document.getElementById('appAlertConfirmBtn');
|
||||||
|
const safeActions = new Set([
|
||||||
|
document.getElementById('appAlertCancelBtn'),
|
||||||
|
document.getElementById('appAlertAlternateBtn'),
|
||||||
|
document.getElementById('appAlertCloseBtn')
|
||||||
|
]);
|
||||||
|
if (modal.dataset.defaultAction !== 'confirm' || safeActions.has(document.activeElement) || confirm?.disabled) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
confirm?.click();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|||||||
@@ -142,6 +142,52 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; }
|
getManagedOnlineBackupProbeCalls() { return managedOnlineBackupProbeCalls; }
|
||||||
});
|
});
|
||||||
`, 'utf8');
|
`, 'utf8');
|
||||||
|
const appDialogBehaviorScript = `(async () => {
|
||||||
|
setupListeners();
|
||||||
|
const settle = promise => Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise(resolve => setTimeout(() => resolve('pending'), 30))
|
||||||
|
]);
|
||||||
|
const safePromise = showAppConfirm({ title: 'Safe', message: 'Safe default', confirmText: 'Continue', danger: true });
|
||||||
|
const safeFocus = document.activeElement?.id;
|
||||||
|
document.getElementById('appAlertCancelBtn').click();
|
||||||
|
const safeResult = await safePromise;
|
||||||
|
const removalPromise = showAppConfirm({ title: 'Remove', message: 'Remove now', confirmText: 'Remove', danger: true, defaultAction: 'confirm' });
|
||||||
|
const removalFocus = document.activeElement?.id;
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
||||||
|
const removalEnterResult = await settle(removalPromise);
|
||||||
|
if (removalEnterResult === 'pending') document.getElementById('appAlertCancelBtn').click();
|
||||||
|
const cancelPromise = showAppConfirm({ title: 'Remove', message: 'Cancel intentionally', confirmText: 'Remove', danger: true, defaultAction: 'confirm' });
|
||||||
|
document.getElementById('appAlertCancelBtn').focus();
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }));
|
||||||
|
const cancelFocusedEnter = await settle(cancelPromise);
|
||||||
|
document.getElementById('appAlertCancelBtn').click();
|
||||||
|
const cancelResult = await cancelPromise;
|
||||||
|
const realShowAppConfirm = showAppConfirm;
|
||||||
|
const removalCalls = [];
|
||||||
|
showAppConfirm = options => {
|
||||||
|
removalCalls.push({ title: options.title, defaultAction: options.defaultAction });
|
||||||
|
return Promise.resolve(false);
|
||||||
|
};
|
||||||
|
selectedRecentIds.clear();
|
||||||
|
selectedRecentIds.add(1);
|
||||||
|
sessionFilesData = [{ order: 1, link: 'https://example.invalid/a', filename: 'a.mp4', host: 'byse.sx' }];
|
||||||
|
await deleteSelectedRecentFiles();
|
||||||
|
await clearAllRecentFiles();
|
||||||
|
queueJobs = [{ id: 'dialog-remove', file: 'C:/dialog-remove.mp4', fileName: 'dialog-remove.mp4', hoster: 'byse.sx', status: 'queued' }];
|
||||||
|
selectedJobIds.clear();
|
||||||
|
selectedJobIds.add('dialog-remove');
|
||||||
|
rebuildJobIndex();
|
||||||
|
await handleContextAction('delete-selected');
|
||||||
|
await handleContextAction('delete-all');
|
||||||
|
showAppConfirm = realShowAppConfirm;
|
||||||
|
queueJobs = [];
|
||||||
|
selectedJobIds.clear();
|
||||||
|
selectedRecentIds.clear();
|
||||||
|
sessionFilesData = [];
|
||||||
|
rebuildJobIndex();
|
||||||
|
return { safeFocus, safeResult, removalFocus, removalEnterResult, cancelFocusedEnter, cancelResult, removalCalls };
|
||||||
|
})()`;
|
||||||
const onlineBackupBehaviorScript = `(async () => {
|
const onlineBackupBehaviorScript = `(async () => {
|
||||||
const ids = {
|
const ids = {
|
||||||
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
a: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||||
@@ -344,6 +390,7 @@ app.whenReady().then(async () => {
|
|||||||
const middleY = Math.floor(size.height / 2);
|
const middleY = Math.floor(size.height / 2);
|
||||||
await window.loadFile(process.env.MHU_RENDERER_PATH, { query: { language: 'en', version: '2.1.31' } });
|
await window.loadFile(process.env.MHU_RENDERER_PATH, { query: { language: 'en', version: '2.1.31' } });
|
||||||
const liveSpeedChart = await window.webContents.executeJavaScript('({ baselinePresent: Boolean(document.querySelector(".upload-speed-baseline")), canvasWidth: document.getElementById("uploadSpeedCanvas")?.getBoundingClientRect().width || 0 })');
|
const liveSpeedChart = await window.webContents.executeJavaScript('({ baselinePresent: Boolean(document.querySelector(".upload-speed-baseline")), canvasWidth: document.getElementById("uploadSpeedCanvas")?.getBoundingClientRect().width || 0 })');
|
||||||
|
const appDialogBehavior = await window.webContents.executeJavaScript(${JSON.stringify(appDialogBehaviorScript)});
|
||||||
const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)});
|
const onlineBackupBehavior = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupBehaviorScript)});
|
||||||
const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)});
|
const onlineBackupLayout = await window.webContents.executeJavaScript(${JSON.stringify(onlineBackupLayoutScript)});
|
||||||
window.setContentSize(760, Math.min(900, display.workAreaSize.height));
|
window.setContentSize(760, Math.min(900, display.workAreaSize.height));
|
||||||
@@ -360,6 +407,7 @@ app.whenReady().then(async () => {
|
|||||||
leftEdge: pixelAt(bitmap, size.width, 0, middleY),
|
leftEdge: pixelAt(bitmap, size.width, 0, middleY),
|
||||||
rightEdge: pixelAt(bitmap, size.width, size.width - 1, middleY),
|
rightEdge: pixelAt(bitmap, size.width, size.width - 1, middleY),
|
||||||
liveSpeedChart,
|
liveSpeedChart,
|
||||||
|
appDialogBehavior,
|
||||||
onlineBackupBehavior,
|
onlineBackupBehavior,
|
||||||
onlineBackupLayout,
|
onlineBackupLayout,
|
||||||
onlineBackupNarrowLayout
|
onlineBackupNarrowLayout
|
||||||
@@ -402,6 +450,20 @@ app.whenReady().then(async () => {
|
|||||||
assert.ok(result.rightEdge[0] > result.rightEdge[1] && result.rightEdge[2] > result.rightEdge[1]);
|
assert.ok(result.rightEdge[0] > result.rightEdge[1] && result.rightEdge[2] > result.rightEdge[1]);
|
||||||
assert.ok(result.liveSpeedChart.canvasWidth > 0);
|
assert.ok(result.liveSpeedChart.canvasWidth > 0);
|
||||||
assert.equal(result.liveSpeedChart.baselinePresent, false);
|
assert.equal(result.liveSpeedChart.baselinePresent, false);
|
||||||
|
assert.deepEqual(result.appDialogBehavior, {
|
||||||
|
safeFocus: 'appAlertCancelBtn',
|
||||||
|
safeResult: false,
|
||||||
|
removalFocus: 'appAlertConfirmBtn',
|
||||||
|
removalEnterResult: true,
|
||||||
|
cancelFocusedEnter: 'pending',
|
||||||
|
cancelResult: false,
|
||||||
|
removalCalls: [
|
||||||
|
{ title: 'Ausgewählte Einträge entfernen?', defaultAction: 'confirm' },
|
||||||
|
{ title: 'Alle Links entfernen?', defaultAction: 'confirm' },
|
||||||
|
{ title: 'Uploads entfernen?', defaultAction: 'confirm' },
|
||||||
|
{ title: 'Alle Uploads entfernen?', defaultAction: 'confirm' }
|
||||||
|
]
|
||||||
|
});
|
||||||
assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']);
|
assert.deepEqual(result.onlineBackupBehavior.initialKeys, ['MHU2-ZYXW…9876', 'MHU2-ABCD…1234']);
|
||||||
assert.deepEqual(result.onlineBackupBehavior.initialWarning, {
|
assert.deepEqual(result.onlineBackupBehavior.initialWarning, {
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user