fix(modal): replace native prompt() with custom modal dialogs
Native browser prompt() is unreliable on mobile browsers and PWAs, often requiring multiple clicks to close. Replace all prompt() calls with custom promptModal() and selectModal() functions that use the existing modal system with proper focus management and animations. Affected pages: shopping (create/rename list), tasks (add subtask), meals (choose shopping list). Fixes #12
This commit is contained in:
@@ -276,6 +276,152 @@ export function closeModal() {
|
||||
_doClose();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// promptModal - Ersatz für native prompt()
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Öffnet ein Modal mit Textfeld als Ersatz für native prompt().
|
||||
* Gibt ein Promise zurück: string bei OK, null bei Cancel/Escape.
|
||||
*
|
||||
* @param {string} label - Beschriftung / Frage
|
||||
* @param {string} [defaultValue=''] - Vorausgefüllter Wert
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
export function promptModal(label, defaultValue = '') {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
|
||||
function finish(value) {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
closeModal();
|
||||
resolve(value);
|
||||
}
|
||||
|
||||
openModal({
|
||||
title: label,
|
||||
size: 'sm',
|
||||
content: `
|
||||
<form id="prompt-modal-form" class="form-stack">
|
||||
<div class="form-field">
|
||||
<input class="form-input" id="prompt-modal-input" type="text"
|
||||
value="${defaultValue.replace(/"/g, '"')}" autocomplete="off">
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn--ghost" id="prompt-modal-cancel">${t('common.cancel')}</button>
|
||||
<button type="submit" class="btn btn--primary" id="prompt-modal-ok">${t('common.save')}</button>
|
||||
</div>
|
||||
</form>`,
|
||||
onSave(panel) {
|
||||
const form = panel.querySelector('#prompt-modal-form');
|
||||
const input = panel.querySelector('#prompt-modal-input');
|
||||
const cancel = panel.querySelector('#prompt-modal-cancel');
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
finish(input.value.trim() || null);
|
||||
});
|
||||
|
||||
cancel.addEventListener('click', () => finish(null));
|
||||
|
||||
// Escape soll null liefern (closeModal wird über onEscape bereits ausgelöst)
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
finish(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
|
||||
// Overlay-Click soll null liefern
|
||||
const overlay = panel.closest('.modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) finish(null);
|
||||
});
|
||||
}
|
||||
|
||||
// Input fokussieren und Text selektieren
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
input.select();
|
||||
}, 50);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// selectModal - Ersatz für native prompt() mit Auswahlliste
|
||||
// --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Öffnet ein Modal mit Select-Dropdown als Ersatz für native prompt() bei Listenauswahl.
|
||||
*
|
||||
* @param {string} label - Beschriftung / Frage
|
||||
* @param {{ value: string|number, label: string }[]} options - Auswahloptionen
|
||||
* @returns {Promise<string|number|null>}
|
||||
*/
|
||||
export function selectModal(label, options) {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
|
||||
function finish(value) {
|
||||
if (resolved) return;
|
||||
resolved = true;
|
||||
closeModal();
|
||||
resolve(value);
|
||||
}
|
||||
|
||||
const optionsHtml = options
|
||||
.map((o) => `<option value="${String(o.value).replace(/"/g, '"')}">${o.label}</option>`)
|
||||
.join('');
|
||||
|
||||
openModal({
|
||||
title: label,
|
||||
size: 'sm',
|
||||
content: `
|
||||
<form id="select-modal-form" class="form-stack">
|
||||
<div class="form-field">
|
||||
<select class="form-input" id="select-modal-input">${optionsHtml}</select>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn--ghost" id="select-modal-cancel">${t('common.cancel')}</button>
|
||||
<button type="submit" class="btn btn--primary" id="select-modal-ok">${t('common.save')}</button>
|
||||
</div>
|
||||
</form>`,
|
||||
onSave(panel) {
|
||||
const form = panel.querySelector('#select-modal-form');
|
||||
const select = panel.querySelector('#select-modal-input');
|
||||
const cancel = panel.querySelector('#select-modal-cancel');
|
||||
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
finish(select.value);
|
||||
});
|
||||
|
||||
cancel.addEventListener('click', () => finish(null));
|
||||
|
||||
const escHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.removeEventListener('keydown', escHandler);
|
||||
finish(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', escHandler);
|
||||
|
||||
const overlay = panel.closest('.modal-overlay');
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', (e) => {
|
||||
if (e.target === overlay) finish(null);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Inline Blur-Validierung
|
||||
// --------------------------------------------------------
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
import { openModal as openSharedModal, closeModal as closeSharedModal } from '/components/modal.js';
|
||||
import { openModal as openSharedModal, closeModal as closeSharedModal, selectModal } from '/components/modal.js';
|
||||
import { stagger } from '/utils/ux.js';
|
||||
import { t, formatDate } from '/i18n.js';
|
||||
import { esc } from '/utils/html.js';
|
||||
@@ -695,11 +695,10 @@ async function transferMeal(mealId) {
|
||||
let listId = state.lists[0].id;
|
||||
|
||||
if (state.lists.length > 1) {
|
||||
const names = state.lists.map((l, i) => `${i + 1}. ${l.name}`).join('\n');
|
||||
const choice = prompt(`Auf welche Einkaufsliste?\n${names}\nNummer eingeben:`);
|
||||
const n = parseInt(choice, 10);
|
||||
if (!n || n < 1 || n > state.lists.length) return;
|
||||
listId = state.lists[n - 1].id;
|
||||
const options = state.lists.map((l) => ({ value: l.id, label: l.name }));
|
||||
const choice = await selectModal(t('meals.transferToShoppingList'), options);
|
||||
if (choice === null) return;
|
||||
listId = Number(choice);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { api } from '/api.js';
|
||||
import { stagger, vibrate } from '/utils/ux.js';
|
||||
import { t } from '/i18n.js';
|
||||
import { esc } from '/utils/html.js';
|
||||
import { promptModal } from '/components/modal.js';
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Konstanten
|
||||
@@ -571,10 +572,10 @@ function wireTabBar(container) {
|
||||
}
|
||||
|
||||
if (target.dataset.action === 'new-list') {
|
||||
const name = prompt(t('shopping.newListPrompt'));
|
||||
if (!name?.trim()) return;
|
||||
const name = await promptModal(t('shopping.newListPrompt'));
|
||||
if (!name) return;
|
||||
try {
|
||||
const data = await api.post('/shopping', { name: name.trim() });
|
||||
const data = await api.post('/shopping', { name });
|
||||
state.lists.push({ ...data.data, item_total: 0, item_checked: 0 });
|
||||
await switchList(data.data.id, container);
|
||||
} catch (err) {
|
||||
@@ -652,10 +653,10 @@ function wireListContentEvents(container) {
|
||||
|
||||
// ---- Liste umbenennen ----
|
||||
if (action === 'rename-list') {
|
||||
const newName = prompt(t('shopping.renameListPrompt'), state.activeList?.name);
|
||||
if (!newName?.trim() || newName.trim() === state.activeList?.name) return;
|
||||
const newName = await promptModal(t('shopping.renameListPrompt'), state.activeList?.name ?? '');
|
||||
if (!newName || newName === state.activeList?.name) return;
|
||||
try {
|
||||
const data = await api.put(`/shopping/${state.activeListId}`, { name: newName.trim() });
|
||||
const data = await api.put(`/shopping/${state.activeListId}`, { name: newName });
|
||||
const idx = state.lists.findIndex((l) => l.id === state.activeListId);
|
||||
if (idx >= 0) state.lists[idx].name = data.data.name;
|
||||
state.activeList = data.data;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { api } from '/api.js';
|
||||
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
||||
import { openModal as openSharedModal, closeModal, wireBlurValidation, btnSuccess, btnError } from '/components/modal.js';
|
||||
import { openModal as openSharedModal, closeModal, wireBlurValidation, btnSuccess, btnError, promptModal } from '/components/modal.js';
|
||||
import { stagger, vibrate } from '/utils/ux.js';
|
||||
import { t, formatDate } from '/i18n.js';
|
||||
import { esc } from '/utils/html.js';
|
||||
@@ -481,10 +481,10 @@ async function handleDeleteTask(id, container) {
|
||||
}
|
||||
|
||||
async function handleAddSubtask(parentId, container) {
|
||||
const title = prompt(t('tasks.subtaskPrompt'));
|
||||
if (!title?.trim()) return;
|
||||
const title = await promptModal(t('tasks.subtaskPrompt'));
|
||||
if (!title) return;
|
||||
try {
|
||||
await api.post('/tasks', { title: title.trim(), parent_task_id: parentId });
|
||||
await api.post('/tasks', { title, parent_task_id: parentId });
|
||||
await loadTasks(container);
|
||||
} catch (err) {
|
||||
window.oikos.showToast(err.message, 'danger');
|
||||
|
||||
@@ -690,6 +690,18 @@
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.form-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.modal-panel__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user