fix: resolve event-listener leaks and CSS gaps found in code quality audit
- notes.js (Critical): move grid click listener from renderGrid() to render() — was re-registered on every save/pin/delete, causing multiple API calls per user action after several interactions - dashboard.js (Major): introduce AbortController (_fabController) so the anonymous document click listener from initFab() is cancelled on each new render() cycle; also remove the redundant initFab() call on the skeleton render - layout.css (Major): extend .label selector to include .form-label, covering usage in notes.js and settings.js without a mass-rename - test-modal-utils.js (Major): 12 unit tests for wireBlurValidation, btnSuccess, btnError; registered as test:modal-utils in package.json - notes.js (Minor): add btnError() shake feedback to save error handler - calendar.js (Minor): add popup.isConnected guard to closePopup so the listener self-removes correctly after navigation without a click Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -695,7 +695,7 @@ function showEventPopup(ev, anchor) {
|
||||
// Schließen bei Klick außerhalb
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', function closePopup(e) {
|
||||
if (!popup.contains(e.target)) {
|
||||
if (!popup.isConnected || !popup.contains(e.target)) {
|
||||
popup.remove();
|
||||
document.removeEventListener('click', closePopup);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
import { api } from '/api.js';
|
||||
|
||||
// Hält den AbortController des aktuellen FAB-Listeners — wird bei jedem render() erneuert.
|
||||
let _fabController = null;
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Hilfsfunktionen
|
||||
// --------------------------------------------------------
|
||||
@@ -339,7 +342,7 @@ function renderFab() {
|
||||
`;
|
||||
}
|
||||
|
||||
function initFab(container) {
|
||||
function initFab(container, signal) {
|
||||
const fabMain = container.querySelector('#fab-main');
|
||||
const fabActions = container.querySelector('#fab-actions');
|
||||
if (!fabMain) return;
|
||||
@@ -368,7 +371,7 @@ function initFab(container) {
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => { if (open) toggleFab(false); });
|
||||
document.addEventListener('click', () => { if (open) toggleFab(false); }, { signal });
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
@@ -395,6 +398,9 @@ function wireLinks(container) {
|
||||
// --------------------------------------------------------
|
||||
|
||||
export async function render(container, { user }) {
|
||||
_fabController?.abort();
|
||||
_fabController = new AbortController();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="dashboard">
|
||||
<div class="dashboard__grid">
|
||||
@@ -412,7 +418,6 @@ export async function render(container, { user }) {
|
||||
</div>
|
||||
${renderFab()}
|
||||
`;
|
||||
initFab(container);
|
||||
|
||||
let data = { upcomingEvents: [], urgentTasks: [], todayMeals: [], pinnedNotes: [] };
|
||||
let weather = null;
|
||||
@@ -455,6 +460,6 @@ export async function render(container, { user }) {
|
||||
`;
|
||||
|
||||
wireLinks(container);
|
||||
initFab(container);
|
||||
initFab(container, _fabController.signal);
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
|
||||
+17
-18
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { api } from '/api.js';
|
||||
import { openModal as openSharedModal, closeModal } from '/components/modal.js';
|
||||
import { openModal as openSharedModal, closeModal, btnError } from '/components/modal.js';
|
||||
import { stagger, vibrate } from '/utils/ux.js';
|
||||
|
||||
// --------------------------------------------------------
|
||||
@@ -71,6 +71,21 @@ export async function render(container, { user }) {
|
||||
state.notes = [];
|
||||
window.oikos?.showToast('Notizen konnten nicht geladen werden.', 'danger');
|
||||
}
|
||||
const grid = container.querySelector('#notes-grid');
|
||||
grid.addEventListener('click', async (e) => {
|
||||
const pinBtn = e.target.closest('[data-action="pin"]');
|
||||
if (pinBtn) { e.stopPropagation(); await togglePin(parseInt(pinBtn.dataset.id, 10)); return; }
|
||||
|
||||
const delBtn = e.target.closest('[data-action="delete"]');
|
||||
if (delBtn) { e.stopPropagation(); await deleteNote(parseInt(delBtn.dataset.id, 10)); return; }
|
||||
|
||||
const card = e.target.closest('.note-card[data-id]');
|
||||
if (card) {
|
||||
const note = state.notes.find((n) => n.id === parseInt(card.dataset.id, 10));
|
||||
if (note) openNoteModal({ mode: 'edit', note });
|
||||
}
|
||||
});
|
||||
|
||||
renderGrid();
|
||||
|
||||
const addHandler = () => openNoteModal({ mode: 'create' });
|
||||
@@ -107,23 +122,6 @@ function renderGrid() {
|
||||
grid.innerHTML = state.notes.map((n) => renderNoteCard(n)).join('');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
stagger(grid.querySelectorAll('.note-card'));
|
||||
|
||||
grid.addEventListener('click', async (e) => {
|
||||
// Pin
|
||||
const pinBtn = e.target.closest('[data-action="pin"]');
|
||||
if (pinBtn) { e.stopPropagation(); await togglePin(parseInt(pinBtn.dataset.id, 10)); return; }
|
||||
|
||||
// Delete
|
||||
const delBtn = e.target.closest('[data-action="delete"]');
|
||||
if (delBtn) { e.stopPropagation(); await deleteNote(parseInt(delBtn.dataset.id, 10)); return; }
|
||||
|
||||
// Edit
|
||||
const card = e.target.closest('.note-card[data-id]');
|
||||
if (card) {
|
||||
const note = state.notes.find((n) => n.id === parseInt(card.dataset.id, 10));
|
||||
if (note) openNoteModal({ mode: 'edit', note });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderNoteCard(note) {
|
||||
@@ -429,6 +427,7 @@ function openNoteModal({ mode, note = null }) {
|
||||
window.oikos?.showToast(mode === 'create' ? 'Notiz erstellt' : 'Notiz gespeichert', 'success');
|
||||
} catch (err) {
|
||||
window.oikos?.showToast(err.data?.error ?? 'Fehler', 'error');
|
||||
btnError(saveBtn);
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = isEdit ? 'Speichern' : 'Erstellen';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user