feat: shared modal system + migrate tasks module
- Add public/components/modal.js with focus-trap, escape-handler, overlay-click, focus-restore, scroll-lock, aria-modal (Spec §5.1/§5.2) - Migrate tasks.js from custom modal to shared openModal/closeModal API - Remove .modal-backdrop/.modal/.modal__* styles from tasks.css - Add .modal-panel--sm/--lg sizing variants to layout.css Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
|||||||
|
/**
|
||||||
|
* Modul: Shared Modal-System
|
||||||
|
* Zweck: Einheitliches Modal mit Focus-Trap, Escape-Handler, Overlay-Click,
|
||||||
|
* Focus-Restore, Scroll-Lock und aria-modal.
|
||||||
|
* Abhängigkeiten: CSS-Klassen aus layout.css (.modal-overlay, .modal-panel, etc.)
|
||||||
|
*
|
||||||
|
* API:
|
||||||
|
* openModal({ title, content, onSave, onDelete, size }) → void
|
||||||
|
* closeModal() → void
|
||||||
|
*/
|
||||||
|
|
||||||
|
let activeOverlay = null;
|
||||||
|
let previouslyFocused = null;
|
||||||
|
let focusTrapHandler = null;
|
||||||
|
|
||||||
|
const FOCUSABLE = [
|
||||||
|
'a[href]',
|
||||||
|
'button:not([disabled])',
|
||||||
|
'input:not([disabled])',
|
||||||
|
'select:not([disabled])',
|
||||||
|
'textarea:not([disabled])',
|
||||||
|
'[tabindex]:not([tabindex="-1"])',
|
||||||
|
].join(', ');
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// Focus-Trap (Spec §5.2)
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
function trapFocus(container) {
|
||||||
|
focusTrapHandler = (e) => {
|
||||||
|
if (e.key !== 'Tab') return;
|
||||||
|
const focusable = container.querySelectorAll(FOCUSABLE);
|
||||||
|
if (!focusable.length) return;
|
||||||
|
const first = focusable[0];
|
||||||
|
const last = focusable[focusable.length - 1];
|
||||||
|
|
||||||
|
if (e.shiftKey && document.activeElement === first) {
|
||||||
|
e.preventDefault();
|
||||||
|
last.focus();
|
||||||
|
} else if (!e.shiftKey && document.activeElement === last) {
|
||||||
|
e.preventDefault();
|
||||||
|
first.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
container.addEventListener('keydown', focusTrapHandler);
|
||||||
|
|
||||||
|
// Focus first focusable element
|
||||||
|
const first = container.querySelector(FOCUSABLE);
|
||||||
|
if (first) {
|
||||||
|
setTimeout(() => first.focus(), 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// Escape-Handler
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
function onEscape(e) {
|
||||||
|
if (e.key === 'Escape') closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// openModal
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Öffnet ein Modal mit dem Shared-System.
|
||||||
|
*
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {string} opts.title — Titel im Modal-Header
|
||||||
|
* @param {string} opts.content — HTML-String für den Modal-Body
|
||||||
|
* @param {Function} [opts.onSave] — Callback, wird nach Einfügen in DOM aufgerufen
|
||||||
|
* (zum Binden von Form-Events)
|
||||||
|
* @param {Function} [opts.onDelete] — Falls vorhanden, wird ein Löschen-Button eingebaut
|
||||||
|
* @param {string} [opts.size='md'] — 'sm' | 'md' | 'lg'
|
||||||
|
*/
|
||||||
|
export function openModal({ title, content, onSave, onDelete, size = 'md' } = {}) {
|
||||||
|
// Vorheriges Modal schließen (kein Stacking)
|
||||||
|
if (activeOverlay) closeModal();
|
||||||
|
|
||||||
|
// Focus-Restore vorbereiten
|
||||||
|
previouslyFocused = document.activeElement;
|
||||||
|
|
||||||
|
// Scroll-Lock
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
const sizeClass = size !== 'md' ? ` modal-panel--${size}` : '';
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<div class="modal-overlay" id="shared-modal-overlay">
|
||||||
|
<div class="modal-panel${sizeClass}" role="dialog" aria-modal="true"
|
||||||
|
aria-labelledby="shared-modal-title">
|
||||||
|
<div class="modal-panel__header">
|
||||||
|
<h2 class="modal-panel__title" id="shared-modal-title">${title}</h2>
|
||||||
|
<button class="modal-panel__close" data-action="close-modal" aria-label="Schließen">
|
||||||
|
<i data-lucide="x" style="width:18px;height:18px"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-panel__body">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
document.body.insertAdjacentHTML('beforeend', html);
|
||||||
|
activeOverlay = document.getElementById('shared-modal-overlay');
|
||||||
|
|
||||||
|
// Lucide-Icons rendern
|
||||||
|
if (window.lucide) window.lucide.createIcons();
|
||||||
|
|
||||||
|
// Focus-Trap
|
||||||
|
const panel = activeOverlay.querySelector('.modal-panel');
|
||||||
|
trapFocus(panel);
|
||||||
|
|
||||||
|
// Overlay-Click schließt Modal
|
||||||
|
activeOverlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === activeOverlay) closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close-Button
|
||||||
|
activeOverlay.querySelector('[data-action="close-modal"]')
|
||||||
|
?.addEventListener('click', closeModal);
|
||||||
|
|
||||||
|
// Escape
|
||||||
|
document.addEventListener('keydown', onEscape);
|
||||||
|
|
||||||
|
// Callback für Aufrufer (Form-Events binden etc.)
|
||||||
|
if (typeof onSave === 'function') onSave(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// closeModal
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
export function closeModal() {
|
||||||
|
if (!activeOverlay) return;
|
||||||
|
|
||||||
|
document.removeEventListener('keydown', onEscape);
|
||||||
|
|
||||||
|
// Focus-Trap-Handler entfernen
|
||||||
|
if (focusTrapHandler) {
|
||||||
|
const panel = activeOverlay.querySelector('.modal-panel');
|
||||||
|
if (panel) panel.removeEventListener('keydown', focusTrapHandler);
|
||||||
|
focusTrapHandler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeOverlay.remove();
|
||||||
|
activeOverlay = null;
|
||||||
|
|
||||||
|
// Scroll-Lock aufheben
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
|
||||||
|
// Focus-Restore
|
||||||
|
if (previouslyFocused && typeof previouslyFocused.focus === 'function') {
|
||||||
|
previouslyFocused.focus();
|
||||||
|
previouslyFocused = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
-132
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { api } from '/api.js';
|
import { api } from '/api.js';
|
||||||
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
import { renderRRuleFields, bindRRuleEvents, getRRuleValues } from '/rrule-ui.js';
|
||||||
|
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Konstanten
|
// Konstanten
|
||||||
@@ -215,9 +216,8 @@ function renderTaskGroups(tasks, groupMode) {
|
|||||||
// Task-Modal (Erstellen / Bearbeiten)
|
// Task-Modal (Erstellen / Bearbeiten)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function renderModal({ task = null, users = [] } = {}) {
|
function renderModalContent({ task = null, users = [] } = {}) {
|
||||||
const isEdit = !!task;
|
const isEdit = !!task;
|
||||||
const title = isEdit ? 'Aufgabe bearbeiten' : 'Neue Aufgabe';
|
|
||||||
|
|
||||||
const userOptions = users.map((u) =>
|
const userOptions = users.map((u) =>
|
||||||
`<option value="${u.id}" ${task?.assigned_to === u.id ? 'selected' : ''}>${u.display_name}</option>`
|
`<option value="${u.id}" ${task?.assigned_to === u.id ? 'selected' : ''}>${u.display_name}</option>`
|
||||||
@@ -232,93 +232,82 @@ function renderModal({ task = null, users = [] } = {}) {
|
|||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="modal-backdrop" id="task-modal-backdrop">
|
<form id="task-form" novalidate>
|
||||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
<input type="hidden" id="task-id" value="${task?.id ?? ''}">
|
||||||
<div class="modal__header">
|
|
||||||
<h2 class="modal__title" id="modal-title">${title}</h2>
|
|
||||||
<button class="modal__close" data-action="close-modal" aria-label="Schließen">
|
|
||||||
<i data-lucide="x" style="width:18px;height:18px"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="task-form" novalidate>
|
<div class="form-group">
|
||||||
<input type="hidden" id="task-id" value="${task?.id ?? ''}">
|
<label class="label" for="task-title">Titel *</label>
|
||||||
|
<input class="input" type="text" id="task-title" name="title"
|
||||||
<div class="form-group">
|
value="${task?.title ?? ''}" placeholder="Was muss erledigt werden?"
|
||||||
<label class="label" for="task-title">Titel *</label>
|
required autocomplete="off">
|
||||||
<input class="input" type="text" id="task-title" name="title"
|
|
||||||
value="${task?.title ?? ''}" placeholder="Was muss erledigt werden?"
|
|
||||||
required autocomplete="off">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="label" for="task-description">Notiz</label>
|
|
||||||
<textarea class="input" id="task-description" name="description"
|
|
||||||
rows="2" placeholder="Optionale Details…"
|
|
||||||
style="resize:vertical">${task?.description ?? ''}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
|
||||||
<div class="form-group" style="margin-bottom:0">
|
|
||||||
<label class="label" for="task-priority">Priorität</label>
|
|
||||||
<select class="input" id="task-priority" name="priority" style="min-height:44px">
|
|
||||||
${priorityOptions}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:0">
|
|
||||||
<label class="label" for="task-category">Kategorie</label>
|
|
||||||
<select class="input" id="task-category" name="category" style="min-height:44px">
|
|
||||||
${categoryOptions}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-top:var(--space-4)">
|
|
||||||
<div class="form-group" style="margin-bottom:0">
|
|
||||||
<label class="label" for="task-due-date">Fälligkeit</label>
|
|
||||||
<input class="input" type="date" id="task-due-date" name="due_date"
|
|
||||||
value="${task?.due_date ?? ''}">
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:0">
|
|
||||||
<label class="label" for="task-due-time">Uhrzeit</label>
|
|
||||||
<input class="input" type="time" id="task-due-time" name="due_time"
|
|
||||||
value="${task?.due_time ?? ''}">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" style="margin-top:var(--space-4)">
|
|
||||||
<label class="label" for="task-assigned">Zugewiesen an</label>
|
|
||||||
<select class="input" id="task-assigned" name="assigned_to" style="min-height:44px">
|
|
||||||
<option value="">— Niemand —</option>
|
|
||||||
${userOptions}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${isEdit ? `
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="label" for="task-status">Status</label>
|
|
||||||
<select class="input" id="task-status" name="status" style="min-height:44px">
|
|
||||||
${STATUSES.map((s) =>
|
|
||||||
`<option value="${s.value}" ${task.status === s.value ? 'selected' : ''}>${s.label}</option>`
|
|
||||||
).join('')}
|
|
||||||
</select>
|
|
||||||
</div>` : ''}
|
|
||||||
|
|
||||||
${renderRRuleFields('task', task?.recurrence_rule)}
|
|
||||||
|
|
||||||
<div id="task-form-error" class="login-error" hidden></div>
|
|
||||||
|
|
||||||
<div class="modal__actions">
|
|
||||||
${isEdit ? `
|
|
||||||
<button type="button" class="btn btn--danger" data-action="delete-task"
|
|
||||||
data-id="${task.id}">Löschen</button>` : ''}
|
|
||||||
<button type="submit" class="btn btn--primary" id="task-submit-btn">
|
|
||||||
${isEdit ? 'Speichern' : 'Erstellen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="label" for="task-description">Notiz</label>
|
||||||
|
<textarea class="input" id="task-description" name="description"
|
||||||
|
rows="2" placeholder="Optionale Details…"
|
||||||
|
style="resize:vertical">${task?.description ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
||||||
|
<div class="form-group" style="margin-bottom:0">
|
||||||
|
<label class="label" for="task-priority">Priorität</label>
|
||||||
|
<select class="input" id="task-priority" name="priority" style="min-height:44px">
|
||||||
|
${priorityOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0">
|
||||||
|
<label class="label" for="task-category">Kategorie</label>
|
||||||
|
<select class="input" id="task-category" name="category" style="min-height:44px">
|
||||||
|
${categoryOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);margin-top:var(--space-4)">
|
||||||
|
<div class="form-group" style="margin-bottom:0">
|
||||||
|
<label class="label" for="task-due-date">Fälligkeit</label>
|
||||||
|
<input class="input" type="date" id="task-due-date" name="due_date"
|
||||||
|
value="${task?.due_date ?? ''}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0">
|
||||||
|
<label class="label" for="task-due-time">Uhrzeit</label>
|
||||||
|
<input class="input" type="time" id="task-due-time" name="due_time"
|
||||||
|
value="${task?.due_time ?? ''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-top:var(--space-4)">
|
||||||
|
<label class="label" for="task-assigned">Zugewiesen an</label>
|
||||||
|
<select class="input" id="task-assigned" name="assigned_to" style="min-height:44px">
|
||||||
|
<option value="">— Niemand —</option>
|
||||||
|
${userOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${isEdit ? `
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="label" for="task-status">Status</label>
|
||||||
|
<select class="input" id="task-status" name="status" style="min-height:44px">
|
||||||
|
${STATUSES.map((s) =>
|
||||||
|
`<option value="${s.value}" ${task.status === s.value ? 'selected' : ''}>${s.label}</option>`
|
||||||
|
).join('')}
|
||||||
|
</select>
|
||||||
|
</div>` : ''}
|
||||||
|
|
||||||
|
${renderRRuleFields('task', task?.recurrence_rule)}
|
||||||
|
|
||||||
|
<div id="task-form-error" class="login-error" hidden></div>
|
||||||
|
|
||||||
|
<div class="modal-panel__footer" style="padding:0;border:none;margin-top:var(--space-6)">
|
||||||
|
${isEdit ? `
|
||||||
|
<button type="button" class="btn btn--danger" data-action="delete-task"
|
||||||
|
data-id="${task.id}">Löschen</button>` : ''}
|
||||||
|
<button type="submit" class="btn btn--primary" id="task-submit-btn">
|
||||||
|
${isEdit ? 'Speichern' : 'Erstellen'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -367,36 +356,27 @@ async function loadTaskForEdit(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
// Modal-Verwaltung
|
// Modal-Verwaltung (delegiert an Shared Modal-System)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function openModal(html) {
|
function openTaskModal({ task = null, users = [] } = {}, container) {
|
||||||
document.body.insertAdjacentHTML('beforeend', html);
|
const isEdit = !!task;
|
||||||
if (window.lucide) window.lucide.createIcons();
|
openSharedModal({
|
||||||
|
title: isEdit ? 'Aufgabe bearbeiten' : 'Neue Aufgabe',
|
||||||
|
content: renderModalContent({ task, users }),
|
||||||
|
size: 'lg',
|
||||||
|
onSave(panel) {
|
||||||
|
// RRULE-Events binden
|
||||||
|
bindRRuleEvents(document, 'task');
|
||||||
|
|
||||||
// RRULE-Events binden
|
// Form-Events
|
||||||
bindRRuleEvents(document, 'task');
|
panel.querySelector('#task-form')
|
||||||
|
?.addEventListener('submit', (e) => handleFormSubmit(e, container));
|
||||||
|
|
||||||
// Fokus auf erstes Eingabefeld
|
panel.querySelector('[data-action="delete-task"]')
|
||||||
setTimeout(() => document.getElementById('task-title')?.focus(), 50);
|
?.addEventListener('click', (e) => handleDeleteTask(e.currentTarget.dataset.id, container));
|
||||||
|
},
|
||||||
// Backdrop-Klick schließt Modal
|
});
|
||||||
document.getElementById('task-modal-backdrop')
|
|
||||||
.addEventListener('click', (e) => {
|
|
||||||
if (e.target.id === 'task-modal-backdrop') closeModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Escape schließt Modal
|
|
||||||
document.addEventListener('keydown', onEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
document.getElementById('task-modal-backdrop')?.remove();
|
|
||||||
document.removeEventListener('keydown', onEscape);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onEscape(e) {
|
|
||||||
if (e.key === 'Escape') closeModal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -604,8 +584,7 @@ function wireKanbanDrag(container) {
|
|||||||
if (!card) return;
|
if (!card) return;
|
||||||
try {
|
try {
|
||||||
const task = await loadTaskForEdit(card.dataset.taskId);
|
const task = await loadTaskForEdit(card.dataset.taskId);
|
||||||
openModal(renderModal({ task, users: state.users }));
|
openTaskModal({ task, users: state.users }, container);
|
||||||
wireModalEvents(container);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger');
|
window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger');
|
||||||
}
|
}
|
||||||
@@ -720,7 +699,7 @@ function wireSwipeGestures(container) {
|
|||||||
|
|
||||||
row.addEventListener('touchstart', (e) => {
|
row.addEventListener('touchstart', (e) => {
|
||||||
// Geste ignorieren wenn Modal offen
|
// Geste ignorieren wenn Modal offen
|
||||||
if (document.getElementById('task-modal-backdrop')) return;
|
if (document.getElementById('shared-modal-overlay')) return;
|
||||||
startX = e.touches[0].clientX;
|
startX = e.touches[0].clientX;
|
||||||
startY = e.touches[0].clientY;
|
startY = e.touches[0].clientY;
|
||||||
dx = 0;
|
dx = 0;
|
||||||
@@ -800,8 +779,7 @@ function wireSwipeGestures(container) {
|
|||||||
if (navigator.vibrate) navigator.vibrate(20);
|
if (navigator.vibrate) navigator.vibrate(20);
|
||||||
try {
|
try {
|
||||||
const task = await loadTaskForEdit(taskId);
|
const task = await loadTaskForEdit(taskId);
|
||||||
openModal(renderModal({ task, users: state.users }));
|
openTaskModal({ task, users: state.users }, container);
|
||||||
wireModalEvents(container);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger');
|
window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger');
|
||||||
}
|
}
|
||||||
@@ -865,23 +843,12 @@ function wireGroupToggle(container) {
|
|||||||
|
|
||||||
function wireNewTaskBtn(container) {
|
function wireNewTaskBtn(container) {
|
||||||
const handler = () => {
|
const handler = () => {
|
||||||
openModal(renderModal({ users: state.users }));
|
openTaskModal({ users: state.users }, container);
|
||||||
wireModalEvents(container);
|
|
||||||
};
|
};
|
||||||
container.querySelector('#btn-new-task')?.addEventListener('click', handler);
|
container.querySelector('#btn-new-task')?.addEventListener('click', handler);
|
||||||
container.querySelector('#fab-new-task')?.addEventListener('click', handler);
|
container.querySelector('#fab-new-task')?.addEventListener('click', handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wireModalEvents(container) {
|
|
||||||
document.getElementById('task-form')?.addEventListener('submit', (e) => handleFormSubmit(e, container));
|
|
||||||
|
|
||||||
document.querySelector('[data-action="close-modal"]')
|
|
||||||
?.addEventListener('click', closeModal);
|
|
||||||
|
|
||||||
document.querySelector('[data-action="delete-task"]')
|
|
||||||
?.addEventListener('click', (e) => handleDeleteTask(e.currentTarget.dataset.id, container));
|
|
||||||
}
|
|
||||||
|
|
||||||
function wireTaskList(container) {
|
function wireTaskList(container) {
|
||||||
const listEl = container.querySelector('#task-list');
|
const listEl = container.querySelector('#task-list');
|
||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
@@ -922,8 +889,7 @@ function wireTaskList(container) {
|
|||||||
if (action === 'edit-task' || action === 'open-task') {
|
if (action === 'edit-task' || action === 'open-task') {
|
||||||
try {
|
try {
|
||||||
const task = await loadTaskForEdit(id);
|
const task = await loadTaskForEdit(id);
|
||||||
openModal(renderModal({ task, users: state.users }));
|
openTaskModal({ task, users: state.users }, container);
|
||||||
wireModalEvents(container);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger');
|
window.oikos.showToast('Aufgabe konnte nicht geladen werden.', 'danger');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -599,6 +599,8 @@
|
|||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
animation: modal-scale-in 0.2s var(--ease-out) forwards;
|
animation: modal-scale-in 0.2s var(--ease-out) forwards;
|
||||||
}
|
}
|
||||||
|
.modal-panel--sm { max-width: 400px; }
|
||||||
|
.modal-panel--lg { max-width: 680px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-panel__header {
|
.modal-panel__header {
|
||||||
|
|||||||
@@ -472,89 +472,6 @@
|
|||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
|
||||||
* Task-Modal (Erstellen / Bearbeiten)
|
|
||||||
* -------------------------------------------------------- */
|
|
||||||
.modal-backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-color: var(--color-overlay);
|
|
||||||
z-index: var(--z-modal);
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
animation: backdrop-in 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.modal-backdrop {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes backdrop-in {
|
|
||||||
from { opacity: 0; }
|
|
||||||
to { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
background-color: var(--color-surface);
|
|
||||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
|
||||||
width: 100%;
|
|
||||||
max-height: 92dvh;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: var(--space-6);
|
|
||||||
padding-bottom: calc(var(--space-6) + var(--safe-area-inset-bottom));
|
|
||||||
animation: modal-in 0.25s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.modal {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
max-width: 540px;
|
|
||||||
max-height: 85dvh;
|
|
||||||
padding-bottom: var(--space-6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modal-in {
|
|
||||||
from { transform: translateY(24px); opacity: 0; }
|
|
||||||
to { transform: translateY(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: var(--space-5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__title {
|
|
||||||
font-size: var(--text-xl);
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__close {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: var(--radius-full);
|
|
||||||
background-color: var(--color-surface-2);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
min-height: unset;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-3);
|
|
||||||
margin-top: var(--space-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal__actions .btn { flex: 1; }
|
|
||||||
|
|
||||||
/* --------------------------------------------------------
|
/* --------------------------------------------------------
|
||||||
* Overdue-Badge (Navigation)
|
* Overdue-Badge (Navigation)
|
||||||
* -------------------------------------------------------- */
|
* -------------------------------------------------------- */
|
||||||
|
|||||||
Reference in New Issue
Block a user