ToDo-App mit JavaScript
Aufgaben speichern, filtern, suchen und sortieren – komplett im Browser, ohne Backend.
Projektüberblick
In diesem Tutorial erstellst du eine kleine ToDo-App mit HTML, CSS und
JavaScript.
Sie läuft komplett im Browser und speichert deine Aufgaben im localStorage.
Was du bauen wirst
- Aufgaben hinzufügen, bearbeiten, löschen
- Als erledigt markieren
- Drei Prioritätsstufen mit farbiger Anzeige
- Automatische Speicherung im Browser
- Filter: Alle / Offen / Erledigt
- Suche mit Debounce und Highlighting
- Tags für flexible Kategorisierung
- Drag & Drop zum Umsortieren
Was du dabei lernst
localStorage– Daten im Browser speichernJSON.stringify()/JSON.parse()– Objekte in Text umwandelnArray.filter()/Array.sort()– Daten filtern und sortieren- DOM-Manipulation mit
createElement()undtextContent - Event Handling mit
addEventListener() - HTML5 Drag & Drop API
App ausprobieren
Hier kannst du die fertige App direkt testen:
Meine Aufgaben
Code verstehen
Was ist localStorage?
localStorage ist eine eingebaute Browser-API - ein
kleiner
Speicher, den jeder Browser für jede Website bereitstellt. Stell es dir vor wie
einen
Notizblock, den der Browser für deine Seite aufbewahrt. Die Daten bleiben auch nach
dem
Schließen des Browsers erhalten.
localStorage kann nur Text speichern, keine
JavaScript-Objekte oder Arrays. Deshalb brauchen wir JSON.stringify() und
JSON.parse() zur Umwandlung.
Ja! localStorage ist 100% privat. Deine ToDos bleiben in deinem Browser auf deinem Gerät - sie werden niemals an den Server übertragen. Außerdem gilt die Same-Origin Policy: Jede Website hat ihren eigenen, isolierten Speicher. Eine andere Website kann nicht auf die Daten dieser Seite zugreifen.
Gut zu wissen: Die Daten existieren nur in diesem einen Browser. Wenn du die Seite auf einem anderen Gerät oder in einem anderen Browser öffnest, startest du mit einer leeren Liste.
renderTodos() - Das Herzstück der App
Diese Funktion wird bei jeder Änderung aufgerufen und baut die komplette Ansicht neu auf. Der Ablauf:
function renderTodos() {
// 1. Liste leeren
todoList.innerHTML = "";
// 2. Aufgaben filtern (nach Status und Suchbegriff)
let filteredTodos = todos.filter(todo => { ... });
if (searchQuery) {
filteredTodos = filteredTodos.filter(todo =>
todo.text.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// 3. Nach Priorität sortieren
filteredTodos.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
// 4. Für jede Aufgabe: HTML-Elemente erstellen
filteredTodos.forEach(todo => {
const li = document.createElement("li");
// ... Text, Datum, Priorität, Buttons hinzufügen
todoList.appendChild(li);
});
// 5. Statistik aktualisieren
updateStats();
}
Das Prinzip nennt sich "Re-Rendering": Statt einzelne Elemente zu ändern, wird die ganze Liste neu gezeichnet. Das ist bei kleinen Listen performant genug und macht den Code viel einfacher.
Daten speichern und laden
// Speichern: Array → Text → localStorage
localStorage.setItem("todos", JSON.stringify(todos));
// Aus [{text: "Einkaufen", done: false}]
// wird '[{"text":"Einkaufen","done":false}]'
// Laden: localStorage → Text → Array
let todos = JSON.parse(localStorage.getItem("todos")) || [];
// Falls nichts gespeichert ist (null), starte mit leerem Array []
Aufgabe hinzufügen
function addNewTodo() {
const text = todoInput.value.trim(); // Eingabe holen, Leerzeichen entfernen
if (text === "") return; // Leere Eingaben ignorieren
todos.push({
text: text, // Der Aufgabentext
done: false, // Noch nicht erledigt
priority: todoPriority.value, // Priorität aus Dropdown
createdAt: new Date().toLocaleDateString("de-DE") // Datum
});
todoInput.value = ""; // Eingabefeld leeren
todoPriority.value = "medium"; // Priorität zurücksetzen
saveTodos(); // Speichern und neu rendern
}
Prioritäten und Sortierung
Jede Aufgabe bekommt eine Priorität (hoch/mittel/niedrig). Mit Array.sort()
sortieren wir die Aufgaben so, dass hohe Prioritäten oben erscheinen:
// Sortier-Reihenfolge: Je kleiner die Zahl, desto weiter oben
const priorityOrder = { high: 1, medium: 2, low: 3 };
filteredTodos.sort((a, b) => {
const prioA = priorityOrder[a.priority] || 2; // Fallback: medium
const prioB = priorityOrder[b.priority] || 2;
return prioA - prioB; // Aufsteigend sortieren
});
Mit CSS wird die Priorität visuell angezeigt - ein farbiger linker Rand:
#todoList li.priority-high {
border-left: 4px solid #dc3545; /* Rot */
}
#todoList li.priority-medium {
border-left: 4px solid #ffc107; /* Gelb */
}
#todoList li.priority-low {
border-left: 4px solid #28a745; /* Grün */
}
Filter-Funktion
Mit Array.filter() können wir das Array nach bestimmten
Kriterien filtern:
let currentFilter = "all"; // Mögliche Werte: "all", "open", "done"
const filteredTodos = todos.filter(todo => {
if (currentFilter === "done") return todo.done; // Nur erledigte
if (currentFilter === "open") return !todo.done; // Nur offene
return true; // Alle anzeigen
});
Die filter()-Methode erstellt ein neues Array mit allen
Elementen, die den Test bestehen (also true
zurückgeben).
Aufgabe bearbeiten (Modal)
Statt eines einfachen prompt() nutzen wir ein
Modal-Dialog,
in dem Text und Priorität bearbeitet werden können:
function editTodo(index) {
const todo = todos[index];
// Modal-Elemente erstellen
const overlay = document.createElement("div");
overlay.className = "todo-modal-overlay";
const modal = document.createElement("div");
modal.className = "todo-modal";
// Modal-HTML (statischer Inhalt, kein User-Input!)
modal.innerHTML = `
<h4>Aufgabe bearbeiten</h4>
<div class="todo-modal-field">
<label>Aufgabe:</label>
<input type="text" id="editText">
</div>
<div class="todo-modal-field">
<label>Priorität:</label>
<select id="editPriority">...</select>
</div>
...
`;
// WICHTIG: Werte sicher setzen (nicht via innerHTML!)
const editText = modal.querySelector("#editText");
editText.value = todo.text; // .value ist sicher wie textContent
}
Das Modal-HTML enthält nur statischen Code (Labels, Inputs) - keinen User-Input.
Der eigentliche Aufgabentext wird danach via .value
gesetzt,
was genauso sicher ist wie textContent.
Alle Aufgaben löschen (Reset)
function clearAllTodos() {
// confirm() zeigt einen Bestätigungsdialog
if (confirm("Wirklich alle Aufgaben löschen?")) {
localStorage.removeItem("todos"); // Eintrag aus localStorage entfernen
todos = []; // Array leeren
renderTodos(); // Ansicht aktualisieren
}
}
Statistik berechnen
function updateStats() {
const total = todos.length; // Gesamtzahl
const done = todos.filter(t => t.done).length; // Erledigte zählen
const open = total - done; // Offene = Gesamt - Erledigt
todoStats.textContent = `${done} von ${total} erledigt (${open} offen)`;
}
Sicherheit: textContent vs innerHTML
In der App nutzen wir span.textContent = todo.text; -
das ist wichtig für die Sicherheit!
innerHTML interpretiert HTML-Tags und führt sie aus.
Wenn jemand <script>alert('böse')</script>
als
Aufgabe eingibt, würde der Code ausgeführt werden (XSS-Angriff).
textContent hingegen behandelt alles als reinen Text
-
der HTML-Code wird harmlos angezeigt statt ausgeführt.
Die goldene Regel: innerHTML ist nur gefährlich, wenn
du
User-Input damit einfügst. Für Inhalte, die du selbst
kontrollierst,
ist
es kein Problem:
todoList.innerHTML = "";→ Sicher nur ein leerer Stringbtn.innerHTML = '<svg>...</svg>';→ Sicher dein eigener SVG-Codediv.innerHTML = todo.text;→ Gefährlich User-Input!
CSS: Lange Wörter umbrechen
Damit sehr lange Wörter nicht aus dem Container laufen, nutzen wir diese CSS-Eigenschaften:
.todo-text {
word-break: break-word; /* Erzwingt Umbruch bei langen Wörtern */
overflow-wrap: break-word; /* Fallback für ältere Browser */
hyphens: auto; /* Automatische Silbentrennung (optional) */
}
.todo-content {
min-width: 0; /* Wichtig! Ohne dies ignoriert Flexbox den Umbruch */
}
Problem: Filter und Original-Index
Wenn wir filtern, stimmen die Indizes im gefilterten Array nicht mehr mit dem Original überein:
// Original: todos = [{text: "A"}, {text: "B", done: true}, {text: "C"}]
// Gefiltert (nur done): [{text: "B", done: true}]
// Index 0 im gefilterten Array ≠ Index 0 im Original!
// Lösung: Original-Index suchen
filteredTodos.forEach(todo => {
const originalIndex = todos.indexOf(todo); // Findet Position im Original
// Jetzt können wir toggleDone(originalIndex) korrekt aufrufen
});
Suche mit Debounce
Die Suche filtert Aufgaben nach dem eingegebenen Text. Um nicht bei jedem Tastendruck zu suchen (was bei vielen Aufgaben langsam wäre), nutzen wir Debounce:
let searchTimeout;
todoSearch.addEventListener("input", e => {
clearTimeout(searchTimeout); // Vorherigen Timer abbrechen
searchTimeout = setTimeout(() => { // Neuen Timer starten
searchQuery = e.target.value.trim();
renderTodos();
}, 300); // 300ms warten nach letztem Tippen
});
Das bedeutet: Die Suche startet erst 300ms nachdem der User aufgehört hat zu tippen.
Suchbegriff hervorheben (sicher!)
Beim Hervorheben des Suchbegriffs könnten wir innerHTML
nutzen - aber das wäre bei User-Input gefährlich. Stattdessen erstellen wir die
Elemente
sicher:
// Text in Teile aufsplitten (am Suchbegriff)
const parts = todo.text.split(regex);
parts.forEach(part => {
if (part.toLowerCase() === searchQuery.toLowerCase()) {
// Treffer: Als <mark> Element einfügen
const mark = document.createElement("mark");
mark.textContent = part; // textContent = sicher!
span.appendChild(mark);
} else {
// Kein Treffer: Als reiner Text einfügen
span.appendChild(document.createTextNode(part));
}
});
So bleibt die Hervorhebung sicher, auch wenn der User HTML-Code als Aufgabe eingibt.
Regex-Sonderzeichen escapen
Wenn der User nach .* sucht, würde das als Regex
interpretiert werden und alles matchen. Deshalb escapen wir Sonderzeichen:
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// Beispiel: "test.*" wird zu "test\.\*"
// Jetzt wird nach dem literalen Text gesucht
Weitere Hilfsfunktionen
Diese kleineren Funktionen sind selbsterklärend, hier ein kurzer Überblick:
// Aufgabe als erledigt/offen markieren (Toggle)
function toggleDone(index) {
todos[index].done = !todos[index].done; // true ↔ false umschalten
saveTodos();
}
// Aufgabe löschen
function deleteTodo(index) {
todos.splice(index, 1); // Element an Position entfernen
saveTodos();
}
// Filter setzen und Buttons aktualisieren
function setFilter(filter) {
currentFilter = filter;
// classList.toggle(klasse, bedingung) - fügt hinzu wenn true, entfernt wenn false
filterAll.classList.toggle("active", filter === "all");
filterOpen.classList.toggle("active", filter === "open");
filterDone.classList.toggle("active", filter === "done");
renderTodos();
}
// Speichern und Ansicht aktualisieren
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
renderTodos(); // Nach jeder Änderung neu zeichnen
}
// Suche leeren (X-Button im Suchfeld)
searchClear.addEventListener("click", () => {
todoSearch.value = ""; // Eingabefeld leeren
searchQuery = ""; // Suchbegriff zurücksetzen
searchClear.classList.remove("visible"); // X-Button ausblenden
renderTodos();
todoSearch.focus(); // Fokus zurück ins Suchfeld
});
Das Muster ist immer gleich: Daten ändern → saveTodos()
aufrufen → Ansicht wird automatisch aktualisiert.
Tags: Flexible Kategorisierung
Statt fester Kategorien können Aufgaben mit #tags
versehen
werden.
Die Tags werden direkt im Aufgabentext eingegeben (z.B. "Milch kaufen #einkaufen
#privat")
und automatisch erkannt:
// Tags aus dem Text extrahieren
function extractTags(text) {
const tagRegex = /#(\w+)/g; // Findet alle #wörter
const tags = [];
let match;
while ((match = tagRegex.exec(text)) !== null) {
tags.push(match[1].toLowerCase()); // Nur den Tag-Namen (ohne #)
}
return [...new Set(tags)]; // Duplikate entfernen
}
// Text ohne Tags (für saubere Anzeige)
function getTextWithoutTags(text) {
return text.replace(/#\w+/g, "").trim().replace(/\s+/g, " ");
}
// Alle verwendeten Tags sammeln (für Filter-Buttons)
function getAllTags() {
const allTags = new Set();
todos.forEach(todo => {
extractTags(todo.text).forEach(tag => allTags.add(tag));
});
return [...allTags].sort();
}
Die Tag-Filter-Buttons werden dynamisch generiert - nur Tags, die tatsächlich verwendet werden, erscheinen als Filter:
function renderTagFilters() {
const allTags = getAllTags();
if (allTags.length === 0) {
tagFilterContainer.style.display = "none";
return;
}
tagFilterContainer.style.display = "flex";
tagFilterContainer.innerHTML = "";
// "Alle" Button
const allBtn = document.createElement("button");
allBtn.className = "tag-filter-btn" + (currentTagFilter === null ? " active" : "");
allBtn.textContent = "Alle";
allBtn.onclick = () => { currentTagFilter = null; renderTodos(); };
tagFilterContainer.appendChild(allBtn);
// Button für jeden Tag
allTags.forEach(tag => {
const btn = document.createElement("button");
btn.className = "tag-filter-btn" + (currentTagFilter === tag ? " active" : "");
btn.textContent = "#" + tag;
btn.onclick = () => { currentTagFilter = tag; renderTodos(); };
tagFilterContainer.appendChild(btn);
});
}
Erweiterter Filter: Prioritäten
Zusätzlich zum Status-Filter (Alle/Offen/Erledigt) gibt es jetzt einen Prioritäts-Filter. Beide Filter können kombiniert werden - z.B. "nur offene dringende Aufgaben":
let currentPriorityFilter = "all"; // "all", "high", "medium", "low"
let currentTagFilter = null; // null oder Tag-String
// In renderTodos() werden alle Filter nacheinander angewendet:
function renderTodos() {
let filteredTodos = todos;
// 1. Status-Filter (Alle/Offen/Erledigt)
filteredTodos = filteredTodos.filter(todo => {
if (currentFilter === "done") return todo.done;
if (currentFilter === "open") return !todo.done;
return true;
});
// 2. Prioritäts-Filter
if (currentPriorityFilter !== "all") {
filteredTodos = filteredTodos.filter(todo =>
todo.priority === currentPriorityFilter
);
}
// 3. Tag-Filter
if (currentTagFilter) {
filteredTodos = filteredTodos.filter(todo => {
const tags = extractTags(todo.text);
return tags.includes(currentTagFilter);
});
}
// 4. Suchbegriff-Filter
if (searchQuery) { ... }
// Rest der Funktion...
}
Drag & Drop Sortierung
Aufgaben können per Drag & Drop innerhalb der gleichen Prioritätsstufe umsortiert werden. Das nutzt die native HTML5 Drag & Drop API:
// Jedes Listen-Element bekommt Drag-Attribute
li.draggable = true;
li.dataset.todoId = todo.id; // Eindeutige ID
li.dataset.priority = todo.priority; // Für Prioritäts-Check
// Event Listeners registrieren
li.addEventListener("dragstart", handleDragStart);
li.addEventListener("dragend", handleDragEnd);
li.addEventListener("dragover", handleDragOver);
li.addEventListener("drop", handleDrop);
// Drag Start: Element merken, visuelles Feedback
function handleDragStart(e) {
draggedItem = this;
draggedTodoId = this.dataset.todoId;
this.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
}
// Drop: Nur innerhalb gleicher Priorität erlaubt
function handleDrop(e) {
e.preventDefault();
if (this.dataset.priority !== draggedItem.dataset.priority) {
return; // Verschiedene Prioritäten = kein Drop
}
// Todos über ID finden (nicht Index!)
const draggedTodo = todos.find(t => t.id === parseInt(draggedTodoId));
const targetTodo = todos.find(t => t.id === parseInt(this.dataset.todoId));
// sortOrder neu berechnen...
saveTodos();
}
Die Sortierung verwendet ein sortOrder-Feld im
Todo-Objekt.
Das ermöglicht manuelle Reihenfolge innerhalb der automatischen
Prioritäts-Gruppierung:
// Sortierung: Erst Priorität, dann manuelle Reihenfolge
filteredTodos.sort((a, b) => {
const prioA = priorityOrder[a.priority] || 2;
const prioB = priorityOrder[b.priority] || 2;
if (prioA !== prioB) return prioA - prioB; // Priorität zuerst
// Innerhalb gleicher Priorität: nach sortOrder
const orderA = a.sortOrder !== undefined ? a.sortOrder : Infinity;
const orderB = b.sortOrder !== undefined ? b.sortOrder : Infinity;
return orderA - orderB;
});
Bei Drag & Drop dürfen wir nicht mit Array-Indizes arbeiten! Filter und
Sortierung
ändern die Positionen - der Index 2 im gefilterten Array ist nicht Index 2 im
Original.
Deshalb bekommt jedes Todo eine eindeutige id
(Timestamp),
über die wir es zuverlässig wiederfinden.
Migration: Bestehende Daten erweitern
Wenn User schon Aufgaben gespeichert haben (vor dem Update), fehlen die neuen Felder
id und sortOrder. Die
Migration
ergänzt diese automatisch beim Start:
function migrateTodos() {
let needsSave = false;
todos.forEach((todo, index) => {
if (!todo.id) {
todo.id = Date.now() + index; // Eindeutige ID generieren
needsSave = true;
}
if (todo.sortOrder === undefined) {
todo.sortOrder = index; // Aktuelle Position als Startwert
needsSave = true;
}
});
if (needsSave) {
localStorage.setItem("todos", JSON.stringify(todos));
}
}
// Wird beim Start aufgerufen, vor renderTodos()
migrateTodos();
renderTodos();
So funktioniert das Update nahtlos - alte Aufgaben bekommen die neuen Felder, ohne dass User etwas manuell tun müssen.
Zusammenfassung
Du hast jetzt eine vollwertige ToDo-App gebaut! Die wichtigsten Konzepte waren:
- localStorage: Daten im Browser speichern, die auch nach dem Schließen erhalten bleiben
- Re-Rendering: Bei jeder Änderung die komplette Ansicht neu aufbauen
- textContent statt innerHTML: User-Input sicher anzeigen
- Filter-Verkettung: Mehrere Filter nacheinander anwenden
- Debounce: Performance bei häufigen Events optimieren
- Migration: Bestehende Daten beim Update erweitern
Ideen zum Weiterbauen
- Fälligkeitsdatum mit Erinnerung
- Export/Import als JSON-Datei
- Mehrere Listen (Arbeit, Privat, Einkaufen...)
- Subtasks (Unteraufgaben)
- Keyboard-Shortcuts (Enter zum Hinzufügen, etc.)
Vollständiger Code
Open HTML
<div class="todo-app-demo grid-2col-bg">
<h3 id="meine-aufgaben">Meine Aufgaben</h3>
<!-- Eingabefeld mit Priorität -->
<div class="todo-input">
<svg class="todo-search-icon" xmlns="http://www.w3.org/2000/svg" width="15" height="15"
viewBox="0 0 15 15" fill="currentColor" aria-hidden="true">
<path
d="M13.449,5.949l-4.097.018c-.166,0-.301-.134-.301-.3V1.619c0-.717-.456-1.393-1.151-1.568-1.027-.258-1.951.515-1.951,1.501l.018,4.097c0,.166-.134.301-.3.301H1.619c-.717,0-1.393.456-1.568,1.151-.258,1.027.515,1.951,1.501,1.951l4.097-.019c.166,0,.301.134.301.3v4.05c0,.717.456,1.393,1.151,1.568,1.027.258,1.951-.515,1.951-1.501l-.019-4.097c0-.166.134-.301.3-.301h4.05c.717,0,1.393-.456,1.568-1.151.258-1.027-.515-1.951-1.501-1.951Z">
</path>
</svg>
<input type="text" id="todoInput"
placeholder="Neue Aufgabe hinzufügen... (#tag für Tags)">
<select id="todoPriority" title="Priorität wählen">
<option value="low" selected>🟢 Normal</option>
<option value="medium">🟡 Wichtig</option>
<option value="high">🔴 Dringend</option>
</select>
<button id="addTodo" class="btn-add">Hinzufügen</button>
</div>
<!-- Suchfeld -->
<div class="todo-search">
<svg class="todo-search-icon" xmlns="http://www.w3.org/2000/svg" width="15" height="15"
viewBox="0 0 15 15" fill="currentColor" aria-hidden="true">
<path
d="M14.633,12.86l-3.179-3.182c-.081-.082-.093-.206-.033-.304.778-1.27,1.072-2.779.81-4.302C11.778,2.443,9.569.364,6.921.046,2.916-.436-.457,2.952.051,6.96c.335,2.639,2.418,4.83,5.041,5.271,1.519.255,3.021-.038,4.284-.812.098-.06.223-.048.305.033l3.178,3.18c.236.237.551.368.887.368s.651-.131.887-.367c.237-.236.368-.551.368-.888,0-.334-.131-.649-.367-.886ZM9.817,6.16c0,2.015-1.64,3.654-3.655,3.654s-3.654-1.639-3.654-3.654,1.639-3.653,3.654-3.653,3.655,1.639,3.655,3.653Z">
</path>
</svg>
<input type="text" id="todoSearch" placeholder="Aufgaben durchsuchen...">
<button type="button" class="todo-search-clear" id="searchClear"
title="Suche leeren">✕</button>
</div>
<!-- Filter-Bereich -->
<div class="todo-filters">
<!-- Status-Filter (Buttons) -->
<div class="todo-filter-buttons">
<button id="filterAll">Alle</button>
<button id="filterOpen" class="active">Offen</button>
<button id="filterDone">Erledigt</button>
</div>
<!-- NEU: Prioritäts-Filter (Dropdown) -->
<div class="todo-priority-filter">
<select id="filterPriority" title="Nach Priorität filtern">
<option value="all">Alle Prioritäten</option>
<option value="high">🔴 Dringend</option>
<option value="medium">🟡 Wichtig</option>
<option value="low">🟢 Normal</option>
</select>
</div>
</div>
<!-- NEU: Tag-Filter (dynamisch generiert) -->
<div class="todo-tag-filters" id="tagFilterContainer"></div>
<!-- Aufgabenliste -->
<ul class="pt-md" id="todoList"></ul>
<!-- Statistik -->
<div class="todo-stats" id="todoStats"></div>
<!-- Reset-Bereich -->
<div class="todo-reset-section">
<button id="resetTodos"><svg xmlns="http://www.w3.org/2000/svg" width="11.46"
height="15" viewBox="0 0 11.46 15" fill="#fff" class="icon-va-y-2 mr-sm"
aria-hidden="true">
<path
d="M10.856,3.38H.604c-.124,0-.221.106-.21.229l.897,10.425c.046.546.503.966,1.051.966h6.775c.548,0,1.005-.42,1.051-.966l.897-10.425c.01-.123-.087-.229-.21-.229ZM6.687,9.226l1.554,1.554c.231.231.231.606,0,.837-.231.231-.606.231-.837,0l-1.554-1.554c-.066-.066-.173-.066-.24,0l-1.554,1.554c-.231.231-.606.231-.837,0h0c-.231-.231-.231-.606,0-.837l1.554-1.554c.066-.066.066-.173,0-.24l-1.554-1.554c-.231-.231-.231-.606,0-.837h0c.231-.231.606-.231.837,0l1.554,1.554c.066.066.174.066.24,0l1.554-1.554c.231-.231.606-.231.837,0,.231.231.231.606,0,.837l-1.554,1.554c-.066.066-.066.173,0,.24Z">
</path>
<path
d="M10.562,1.33h-2.715v-.141c0-.656-.534-1.19-1.189-1.19h-1.8c-.656,0-1.189.534-1.189,1.19v.141H.899c-.497,0-.899.402-.899.899v.323c0,.117.094.211.211.211h11.038c.116,0,.211-.094.211-.211v-.323c0-.496-.402-.899-.899-.899ZM7.284,1.33h-3.054v-.141c0-.346.281-.627.627-.627h1.8c.346,0,.627.281.627.627v.141Z">
</path>
</svg> Alle Aufgaben löschen</button>
</div>
</div>
Open JavaScript
// ===== DOM-Elemente =====
const todoInput = document.getElementById("todoInput");
const todoPriority = document.getElementById("todoPriority");
const todoSearch = document.getElementById("todoSearch");
const searchClear = document.getElementById("searchClear");
const addTodoBtn = document.getElementById("addTodo");
const todoList = document.getElementById("todoList");
const todoStats = document.getElementById("todoStats");
const resetBtn = document.getElementById("resetTodos");
const filterAll = document.getElementById("filterAll");
const filterOpen = document.getElementById("filterOpen");
const filterDone = document.getElementById("filterDone");
// ===== NEUE DOM-Elemente =====
const filterPriority = document.getElementById("filterPriority");
const tagFilterContainer = document.getElementById("tagFilterContainer");
// ===== State =====
let todos = JSON.parse(localStorage.getItem("todos")) || [];
let currentFilter = "open"; // "all", "open", "done" - GEÄNDERT: Standard ist jetzt "open"
let searchQuery = "";
// ===== NEUER State =====
let currentPriorityFilter = "all"; // "all", "high", "medium", "low"
let currentTagFilter = null; // null oder Tag-String (z.B. "arbeit")
// ===== Prioritäts-Sortierung =====
const priorityOrder = { high: 1, medium: 2, low: 3 };
const priorityLabels = { high: "Dringend", medium: "Wichtig", low: "Normal" };
// ===== Drag & Drop State =====
let draggedItem = null;
let draggedTodoId = null;
// ===== Tags aus Text extrahieren =====
function extractTags(text) {
const tagRegex = /#(\w+)/g;
const tags = [];
let match;
while ((match = tagRegex.exec(text)) !== null) {
tags.push(match[1].toLowerCase());
}
return [...new Set(tags)]; // Duplikate entfernen
}
// ===== Text ohne Tags (für Anzeige) =====
function getTextWithoutTags(text) {
return text.replace(/#\w+/g, "").trim().replace(/\s+/g, " ");
}
// ===== Alle verwendeten Tags sammeln =====
function getAllTags() {
const allTags = new Set();
todos.forEach(todo => {
const tags = extractTags(todo.text);
tags.forEach(tag => allTags.add(tag));
});
return [...allTags].sort();
}
// ===== Tag-Filter-Buttons rendern =====
function renderTagFilters() {
if (!tagFilterContainer) return;
tagFilterContainer.innerHTML = "";
const allTags = getAllTags();
if (allTags.length === 0) {
tagFilterContainer.style.display = "none";
return;
}
tagFilterContainer.style.display = "flex";
// "Alle Tags" Button
const allBtn = document.createElement("button");
allBtn.className = "tag-filter-btn" + (currentTagFilter === null ? " active" : "");
allBtn.textContent = "Alle";
allBtn.onclick = () => {
currentTagFilter = null;
renderTagFilters();
renderTodos();
};
tagFilterContainer.appendChild(allBtn);
// Button für jeden Tag
allTags.forEach(tag => {
const btn = document.createElement("button");
btn.className = "tag-filter-btn" + (currentTagFilter === tag ? " active" : "");
btn.textContent = "#" + tag;
btn.onclick = () => {
currentTagFilter = tag;
renderTagFilters();
renderTodos();
};
tagFilterContainer.appendChild(btn);
});
}
// ===== Aufgaben rendern =====
function renderTodos() {
todoList.innerHTML = "";
// Filtern nach Status
let filteredTodos = todos.filter(todo => {
if (currentFilter === "done") return todo.done;
if (currentFilter === "open") return !todo.done;
return true; // "all"
});
// ===== NEU: Filtern nach Priorität =====
if (currentPriorityFilter !== "all") {
filteredTodos = filteredTodos.filter(todo =>
todo.priority === currentPriorityFilter
);
}
// ===== NEU: Filtern nach Tag =====
if (currentTagFilter) {
filteredTodos = filteredTodos.filter(todo => {
const tags = extractTags(todo.text);
return tags.includes(currentTagFilter);
});
}
// Filtern nach Suchbegriff
if (searchQuery) {
filteredTodos = filteredTodos.filter(todo =>
todo.text.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Sortieren: Erst nach Priorität, dann nach manueller Reihenfolge (sortOrder)
filteredTodos.sort((a, b) => {
const prioA = priorityOrder[a.priority] || 2;
const prioB = priorityOrder[b.priority] || 2;
if (prioA !== prioB) return prioA - prioB;
// Innerhalb gleicher Priorität: nach sortOrder sortieren
const orderA = a.sortOrder !== undefined ? a.sortOrder : Infinity;
const orderB = b.sortOrder !== undefined ? b.sortOrder : Infinity;
return orderA - orderB;
});
// Leere Liste anzeigen
if (filteredTodos.length === 0) {
const emptyMsg = document.createElement("li");
emptyMsg.className = "no-results";
if (searchQuery) {
emptyMsg.textContent = `Keine Aufgaben gefunden für "${searchQuery}"`;
} else if (currentTagFilter) {
emptyMsg.textContent = `Keine Aufgaben mit #${currentTagFilter}`;
} else {
emptyMsg.textContent = currentFilter === "all"
? "Keine Aufgaben vorhanden."
: `Keine ${currentFilter === "done" ? "erledigten" : "offenen"} Aufgaben.`;
}
todoList.appendChild(emptyMsg);
updateStats();
renderTagFilters();
return;
}
// Aufgaben durchgehen (mit Original-Index für Aktionen)
filteredTodos.forEach(todo => {
const originalIndex = todos.indexOf(todo);
const li = document.createElement("li");
li.className = `priority-${todo.priority || "medium"}`;
// ===== NEU: Drag & Drop Attribute =====
li.draggable = true;
li.dataset.todoId = todo.id; // ID wird durch Migration garantiert
li.dataset.priority = todo.priority || "medium";
// Content-Bereich (Text + Meta-Infos)
const content = document.createElement("div");
content.className = "todo-content";
// Aufgabentext (ohne Tags)
const span = document.createElement("span");
span.className = "todo-text" + (todo.done ? " done" : "");
const displayText = getTextWithoutTags(todo.text);
// Suchbegriff hervorheben (sicher!)
if (searchQuery && displayText.toLowerCase().includes(searchQuery.toLowerCase())) {
const regex = new RegExp(`(${escapeRegex(searchQuery)})`, "gi");
const parts = displayText.split(regex);
parts.forEach(part => {
if (part.toLowerCase() === searchQuery.toLowerCase()) {
const mark = document.createElement("mark");
mark.className = "search-highlight";
mark.textContent = part;
span.appendChild(mark);
} else {
span.appendChild(document.createTextNode(part));
}
});
} else {
span.textContent = displayText;
}
span.onclick = () => toggleDone(originalIndex);
content.appendChild(span);
// ===== NEU: Tags als Badges anzeigen =====
const tags = extractTags(todo.text);
if (tags.length > 0) {
const tagsContainer = document.createElement("div");
tagsContainer.className = "todo-tags";
tags.forEach(tag => {
const tagBadge = document.createElement("span");
tagBadge.className = "todo-tag-badge" + (currentTagFilter === tag ? " active" : "");
tagBadge.textContent = "#" + tag;
tagBadge.onclick = (e) => {
e.stopPropagation();
currentTagFilter = currentTagFilter === tag ? null : tag;
renderTagFilters();
renderTodos();
};
tagsContainer.appendChild(tagBadge);
});
content.appendChild(tagsContainer);
}
// Meta-Infos (Datum + Priorität)
const meta = document.createElement("div");
meta.className = "todo-meta";
if (todo.createdAt) {
const dateSpan = document.createElement("span");
dateSpan.textContent = "Erstellt: " + todo.createdAt;
meta.appendChild(dateSpan);
}
// Prioritäts-Badge
const prioBadge = document.createElement("span");
prioBadge.className = `todo-priority-badge ${todo.priority || "medium"}`;
prioBadge.textContent = priorityLabels[todo.priority] || "Wichtig";
meta.appendChild(prioBadge);
content.appendChild(meta);
li.appendChild(content);
// Aktions-Buttons
const actions = document.createElement("div");
actions.className = "todo-actions";
// ===== NEU: Drag-Handle =====
const dragHandle = document.createElement("span");
dragHandle.className = "todo-drag-handle";
dragHandle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="15" viewBox="0 0 12 15" fill="currentColor" aria-hidden="true">
<circle cx="2.5" cy="2" r="1.5"/>
<circle cx="9.5" cy="2" r="1.5"/>
<circle cx="2.5" cy="7.5" r="1.5"/>
<circle cx="9.5" cy="7.5" r="1.5"/>
<circle cx="2.5" cy="13" r="1.5"/>
<circle cx="9.5" cy="13" r="1.5"/>
</svg>`;
dragHandle.title = "Ziehen zum Sortieren";
actions.appendChild(dragHandle);
// Bearbeiten-Button
const editBtn = document.createElement("button");
editBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15" fill="var(--text-primary)" aria-hidden="true"><path d="M9.32,2.814c-.046-.045-.119-.045-.165,0L1.52,10.449c-.076.076-.076.2,0,.276l2.824,2.824c.076.076.2.076.276,0l7.635-7.635c.045-.046.045-.119,0-.165l-2.935-2.936Z" />
<path d="M14.83,2.184l-1.945-1.945c-.319-.319-.836-.319-1.155,0l-1.671,1.671c-.115.114-.115.3,0,.415l2.686,2.686c.115.115.3.115.415,0l1.67-1.67c.319-.319.319-.836,0-1.155Z" />
<path d="M1.023,11.39c-.102-.102-.276-.062-.323.075L.007,14.694c-.049.228.154.43.382.38l3.216-.705c.137-.047.177-.221.075-.323l-2.657-2.657Z" /> </svg>`;
// editBtn.textContent = "✏️";
editBtn.title = "Bearbeiten";
editBtn.onclick = () => editTodo(originalIndex);
actions.appendChild(editBtn);
// Löschen-Button
const delBtn = document.createElement("button");
delBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15" fill="var(--color-error)" aria-hidden="true"><path d="M14.516,12.177c.646.646.646,1.693,0,2.338h0c-.646.646-1.693.646-2.339,0l-4.342-4.342c-.185-.185-.484-.185-.669,0l-4.342,4.342c-.646-.646-1.693.646-2.338,0h0c-.646-.646-.646-1.693,0-2.339l4.343-4.342c.185-.185.185-.484,0-.669L.484,2.823c-.646-.646-.646-1.693,0-2.338h0c.646-.646,1.693-.646,2.338,0l4.342,4.342c.185.185.484.185.669,0L12.177.484c.646-.646,1.693-.646,2.339,0h0c.646.646.646,1.693,0,2.338l-4.342,4.343c-.185.185-.185.484,0,.669l4.342,4.342Z"/></svg>`;
// delBtn.textContent = "❌";
delBtn.title = "Löschen";
delBtn.onclick = () => deleteTodo(originalIndex);
actions.appendChild(delBtn);
li.appendChild(actions);
// ===== NEU: Drag & Drop Event Listeners =====
li.addEventListener("dragstart", handleDragStart);
li.addEventListener("dragend", handleDragEnd);
li.addEventListener("dragover", handleDragOver);
li.addEventListener("drop", handleDrop);
li.addEventListener("dragenter", handleDragEnter);
li.addEventListener("dragleave", handleDragLeave);
todoList.appendChild(li);
});
// Statistik und Tag-Filter aktualisieren
updateStats();
renderTagFilters();
}
// ===== Drag & Drop Handlers =====
function handleDragStart(e) {
draggedItem = this;
draggedTodoId = this.dataset.todoId;
this.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", draggedTodoId);
}
function handleDragEnd(e) {
this.classList.remove("dragging");
// Alle drag-over Klassen entfernen
document.querySelectorAll(".drag-over").forEach(el => {
el.classList.remove("drag-over");
});
draggedItem = null;
draggedTodoId = null;
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
}
function handleDragEnter(e) {
e.preventDefault();
// Nur markieren wenn gleiche Priorität
if (this !== draggedItem && this.dataset.priority === draggedItem?.dataset.priority) {
this.classList.add("drag-over");
}
}
function handleDragLeave(e) {
this.classList.remove("drag-over");
}
function handleDrop(e) {
e.preventDefault();
this.classList.remove("drag-over");
if (!draggedItem || this === draggedItem) return;
// Nur innerhalb gleicher Priorität verschieben
if (this.dataset.priority !== draggedItem.dataset.priority) {
return;
}
const draggedId = parseInt(draggedTodoId);
const targetId = parseInt(this.dataset.todoId);
// Finde die Todos über ihre ID (nicht Index!)
const draggedTodo = todos.find(t => t.id === draggedId);
const targetTodo = todos.find(t => t.id === targetId);
if (!draggedTodo || !targetTodo) return;
// Alle Todos der gleichen Priorität holen (sortiert nach aktuellem sortOrder)
const samePrioTodos = todos
.filter(t => t.priority === draggedTodo.priority)
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
// Aktuelle Positionen in dieser Gruppe ermitteln
const draggedPos = samePrioTodos.findIndex(t => t.id === draggedId);
const targetPos = samePrioTodos.findIndex(t => t.id === targetId);
if (draggedPos === -1 || targetPos === -1) return;
// Element aus der Liste entfernen und an neuer Position einfügen
samePrioTodos.splice(draggedPos, 1);
samePrioTodos.splice(targetPos, 0, draggedTodo);
// Neue sortOrder für alle Todos dieser Priorität vergeben
samePrioTodos.forEach((todo, idx) => {
todo.sortOrder = idx;
});
saveTodos();
}
// ===== Regex-Zeichen escapen (für sichere Suche) =====
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// ===== Statistik aktualisieren =====
function updateStats() {
const total = todos.length;
const done = todos.filter(t => t.done).length;
const open = total - done;
const highPrio = todos.filter(t => t.priority === "high" && !t.done).length;
if (total === 0) {
todoStats.textContent = "Füge deine erste Aufgabe hinzu!";
} else {
let statsText = `${done} von ${total} erledigt (${open} offen)`;
if (highPrio > 0) {
statsText += ` • ${highPrio} dringend`;
}
todoStats.textContent = statsText;
}
}
// ===== Neue Aufgabe hinzufügen =====
function addNewTodo() {
const text = todoInput.value.trim();
if (text === "") return;
// Höchste sortOrder für diese Priorität finden
const priority = todoPriority.value;
const samePrioTodos = todos.filter(t => t.priority === priority);
const maxSortOrder = samePrioTodos.reduce((max, t) =>
Math.max(max, t.sortOrder !== undefined ? t.sortOrder : -1), -1);
todos.push({
id: Date.now(), // Eindeutige ID für Drag & Drop
text: text,
done: false,
priority: priority,
createdAt: new Date().toLocaleDateString("de-DE"),
sortOrder: maxSortOrder + 1
});
todoInput.value = "";
todoPriority.value = "low"; // Zurücksetzen auf Standard
saveTodos();
}
// ===== Aufgabe als erledigt markieren =====
function toggleDone(index) {
todos[index].done = !todos[index].done;
saveTodos();
}
// ===== Aufgabe bearbeiten (Modal) =====
function editTodo(index) {
const todo = todos[index];
// Modal erstellen
const overlay = document.createElement("div");
overlay.className = "todo-modal-overlay";
const modal = document.createElement("div");
modal.className = "todo-modal";
modal.innerHTML = `
<h4>Aufgabe bearbeiten</h4>
<div class="todo-modal-field">
<label for="editText">Aufgabe:</label>
<input type="text" id="editText" value="">
</div>
<div class="todo-modal-field">
<label for="editPriority">Priorität:</label>
<select id="editPriority">
<option value="low">🟢 Normal</option>
<option value="medium">🟡 Wichtig</option>
<option value="high">🔴 Dringend</option>
</select>
</div>
<p class="todo-modal-hint">Tipp: Füge Tags mit # hinzu, z.B. #arbeit #privat</p>
<div class="todo-modal-buttons">
<button type="button" class="todo-modal-cancel">Abbrechen</button>
<button type="button" class="todo-modal-save">Speichern</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
// Werte setzen (sicher via DOM, nicht innerHTML!)
const editText = modal.querySelector("#editText");
const editPriority = modal.querySelector("#editPriority");
editText.value = todo.text;
editPriority.value = todo.priority || "medium";
// Fokus auf Textfeld
editText.focus();
editText.select();
// Event Handlers
const closeModal = () => {
document.body.removeChild(overlay);
};
const saveChanges = () => {
const newText = editText.value.trim();
if (newText !== "") {
const oldPriority = todos[index].priority;
todos[index].text = newText;
todos[index].priority = editPriority.value;
// Wenn Priorität geändert, sortOrder neu berechnen
if (oldPriority !== editPriority.value) {
const samePrioTodos = todos.filter(t => t.priority === editPriority.value);
const maxSortOrder = samePrioTodos.reduce((max, t) =>
Math.max(max, t.sortOrder !== undefined ? t.sortOrder : -1), -1);
todos[index].sortOrder = maxSortOrder + 1;
}
saveTodos();
}
closeModal();
};
modal.querySelector(".todo-modal-cancel").onclick = closeModal;
modal.querySelector(".todo-modal-save").onclick = saveChanges;
// Enter zum Speichern, Escape zum Schließen
editText.addEventListener("keydown", e => {
if (e.key === "Enter") saveChanges();
if (e.key === "Escape") closeModal();
});
// Klick auf Overlay schließt Modal
overlay.addEventListener("click", e => {
if (e.target === overlay) closeModal();
});
}
// ===== Aufgabe löschen =====
function deleteTodo(index) {
todos.splice(index, 1);
saveTodos();
}
// ===== Alle Aufgaben löschen =====
function clearAllTodos() {
if (confirm("Wirklich alle Aufgaben löschen?")) {
localStorage.removeItem("todos");
todos = [];
currentTagFilter = null;
renderTodos();
}
}
// ===== Filter setzen =====
function setFilter(filter) {
currentFilter = filter;
// Button-Styles aktualisieren
filterAll.classList.toggle("active", filter === "all");
filterOpen.classList.toggle("active", filter === "open");
filterDone.classList.toggle("active", filter === "done");
renderTodos();
}
// ===== NEU: Prioritäts-Filter setzen =====
function setPriorityFilter(priority) {
currentPriorityFilter = priority;
renderTodos();
}
// ===== Speichern =====
function saveTodos() {
localStorage.setItem("todos", JSON.stringify(todos));
renderTodos();
}
// ===== Event Listeners =====
addTodoBtn.addEventListener("click", addNewTodo);
todoInput.addEventListener("keypress", e => {
if (e.key === "Enter") addNewTodo();
});
resetBtn.addEventListener("click", clearAllTodos);
filterAll.addEventListener("click", () => setFilter("all"));
filterOpen.addEventListener("click", () => setFilter("open"));
filterDone.addEventListener("click", () => setFilter("done"));
// ===== NEU: Prioritäts-Filter Event Listener =====
if (filterPriority) {
filterPriority.addEventListener("change", e => {
setPriorityFilter(e.target.value);
});
}
// Suche mit Debounce (wartet 300ms nach Tippen)
let searchTimeout;
todoSearch.addEventListener("input", e => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
searchQuery = e.target.value.trim();
renderTodos();
}, 300);
// X-Button ein-/ausblenden
searchClear.classList.toggle("visible", e.target.value.length > 0);
});
// Suche leeren
searchClear.addEventListener("click", () => {
todoSearch.value = "";
searchQuery = "";
searchClear.classList.remove("visible");
renderTodos();
todoSearch.focus();
});
// ===== Migration: Bestehende Todos um ID und sortOrder erweitern =====
function migrateTodos() {
let needsSave = false;
todos.forEach((todo, index) => {
if (!todo.id) {
todo.id = Date.now() + index;
needsSave = true;
}
if (todo.sortOrder === undefined) {
todo.sortOrder = index;
needsSave = true;
}
});
if (needsSave) {
localStorage.setItem("todos", JSON.stringify(todos));
}
}
// ===== Initialisierung =====
migrateTodos();
renderTodos();
Open CSS
.todo-app-demo {
max-width: 100%;
}
.todo-input {
position: relative;
display: flex;
gap: 0.5em;
flex-wrap: wrap;
align-items: stretch;
}
.todo-input input[type="text"] {
flex: 1;
min-width: 200px;
padding: 0 0 0 2rem ;
color: var(--text-muted);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
}
.todo-input select {
padding: 0.5em;
color: var(--text-primary);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
}
.btn-add {
padding: 0.75rem 1rem;
font-size: 1rem;
color: white;
background-color: var(--accent-blue);
border: 2px solid var(--accent-blue);
border-radius: 8px;
transition: all 300ms ease;
cursor: pointer;
text-align: center;
box-sizing: border-box;
}
.btn-add:hover {
background-color: var(--accent-blue-hover);
border: 2px solid var(--accent-blue-hover);
}
.todo-search {
margin-top: 1em;
position: relative;
}
.todo-search input {
width: 100%;
padding: 0.5em 2.5em;
color: var(--text-muted);
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-tertiary);
box-sizing: border-box;
}
.todo-search-icon {
position: absolute;
left: 0.75em;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
/* Klicks gehen durch zum Input */
opacity: 0.5;
}
.todo-search-clear {
position: absolute;
right: 0.5em;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 1.2em;
color: var(--text-muted);
padding: 0.25em;
line-height: 1;
display: none;
}
.todo-search-clear:hover {
color: var(--accent-red-dark);
}
.todo-search-clear.visible {
display: block;
}
.todo-filter-buttons {
display: flex;
gap: 0.5em;
margin: 1rem 0;
flex-wrap: wrap;
}
.todo-filter-buttons button {
padding: 0.4em 0.8em;
color: var(--text-primary);
border: 2px solid var(--accent-blue);
background: var(--bg-secondary);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.todo-filter-buttons button:hover {
background: var(--bg-tertiary);
}
.todo-filter-buttons button.active {
background: var(--accent-blue);
color: var(--text-primary);
border-radius: 4px;
border: 2px solid var(--accent-blue);
}
.todo-filter-buttons button.active:hover {
background: var(--accent-blue-hover);
border: 2px solid var(--accent-blue-hover);
}
#todoList {
list-style: none;
padding: 0;
}
#todoList li {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 0.5em;
margin-bottom: 0.5em;
padding: 0.75em;
background: var(--bg-secondary);
border-radius: 0 8px 8px 0;
border: 1px solid var(--border-color);
border-left: 4px solid transparent;
}
/* Prioritäts-Farben */
#todoList li.priority-high {
border-left-color: var(--color-error);
/* background: rgba(220, 53, 69, 0.05); */
background: var(--bg-tertiary);
}
#todoList li.priority-medium {
border-left-color: var(--color-warning);
/* background: rgba(255, 193, 7, 0.05); */
background: var(--bg-tertiary);
}
#todoList li.priority-low {
border-left-color: var(--color-success);
/* background: rgba(40, 167, 69, 0.05); */
background: var(--bg-tertiary);
}
#todoList li.priority-high:hover {
background: rgba(220, 53, 69, 0.05);
}
#todoList li.priority-medium:hover {
background: rgba(255, 193, 7, 0.05);
}
#todoList li.priority-low:hover {
background: rgba(40, 167, 69, 0.05);
}
#todoList li .todo-content {
flex: 1;
min-width: 0;
/* Wichtig für Flexbox-Umbruch */
}
#todoList li .todo-text {
cursor: pointer;
word-break: break-word;
/* Erzwingt Umbruch bei langen Wörtern */
overflow-wrap: break-word;
/* Fallback für ältere Browser */
hyphens: auto;
/* Automatische Silbentrennung */
}
#todoList li .todo-text.done {
text-decoration: line-through;
opacity: 0.6;
}
#todoList li .todo-meta {
font-size: 0.75em;
color: var(--text-muted);
margin-top: 0.25em;
display: flex;
gap: 1em;
flex-wrap: wrap;
}
#todoList li .todo-priority-badge {
font-size: 0.7em;
padding: 0.1em 0.4em;
border-radius: 3px;
font-weight: bold;
text-transform: uppercase;
}
.todo-priority-badge.high {
background: var(--color-error);
color: white;
}
.todo-priority-badge.medium {
background: var(--color-warning);
color: #333;
}
.todo-priority-badge.low {
background: var(--color-success);
color: white;
}
#todoList li .todo-actions {
display: flex;
gap: 0.25em;
flex-shrink: 0;
}
#todoList li button {
background: none;
border: none;
cursor: pointer;
padding: 0.25em;
font-size: 1em;
opacity: 0.7;
transition: opacity 0.2s;
}
#todoList li button:hover {
opacity: 1;
}
.todo-stats {
margin-top: 1em;
padding: 0.5em;
background: var(--bg-tertiary);
border-radius: 4px;
font-size: 0.9em;
}
.todo-reset-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
#resetTodos {
background: var(--color-error);
color: white;
border: none;
padding: 0.5rem 1rem 0.6rem;
border-radius: 4px;
cursor: pointer;
}
#resetTodos:hover {
background: var(--danger-hover, #c82333);
}
.search-highlight {
background: yellow;
padding: 0 2px;
border-radius: 2px;
}
.no-results {
font-style: italic;
opacity: 0.6;
text-align: center;
padding: 1em;
}
/* Modal für Bearbeitung */
.todo-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.todo-modal {
background: var(--bg-primary, white);
padding: 1.5em;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
min-width: 300px;
max-width: 90%;
}
.todo-modal h4 {
margin: 0 0 1em 0;
font-size: 1.1em;
}
.todo-modal-field {
margin-bottom: 1em;
}
.todo-modal-field label {
display: block;
margin-bottom: 0.3em;
font-weight: bold;
font-size: 0.9em;
}
.todo-modal-field input,
.todo-modal-field select {
width: 100%;
padding: 0.5em;
border: 1px solid var(--border-color, #ccc);
border-radius: 4px;
font-size: 1em;
box-sizing: border-box;
}
.todo-modal-buttons {
display: flex;
gap: 0.5em;
justify-content: flex-end;
margin-top: 1.5em;
}
.todo-modal-buttons button {
padding: 0.5em 1em;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.todo-modal-cancel {
background: var(--bg-secondary, #f5f5f5);
border: 1px solid var(--border-color, #ccc);
color: var(--text-primary, #333);
}
.todo-modal-cancel:hover {
background: var(--bg-tertiary, #e0e0e0);
}
.todo-modal-save {
background: var(--accent-color, #007bff);
border: none;
color: white;
}
.todo-modal-save:hover {
background: var(--accent-hover, #0056b3);
}
/* ===== NEUE CSS-KLASSEN für ToDo App v2 =====
Füge diese Styles in deinen oder CSS-Datei ein
================================================== */
/* --- Filter-Bereich Layout --- */
.todo-filters {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
/* --- Prioritäts-Filter Dropdown --- */
.todo-priority-filter select {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color, #ddd);
border-radius: 6px;
background: var(--bg-primary, #fff);
color: var(--text-primary, #333);
font-size: 0.9rem;
cursor: pointer;
}
.todo-priority-filter select:focus {
outline: none;
border-color: var(--color-accent, #007bff);
}
/* --- Tag-Filter Buttons Reihe --- */
.todo-tag-filters {
display: none; /* Wird via JS auf flex gesetzt wenn Tags existieren */
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 8px;
}
.tag-filter-btn {
padding: 0.35rem 0.75rem;
border: 1px solid var(--border-color, #ddd);
border-radius: 20px;
background: var(--bg-primary, #fff);
color: var(--text-secondary, #666);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s ease;
}
.tag-filter-btn:hover {
border-color: var(--color-accent, #007bff);
color: var(--color-accent, #007bff);
}
.tag-filter-btn.active {
background: var(--color-accent, #007bff);
border-color: var(--color-accent, #007bff);
color: #fff;
}
/* --- Tag-Badges in Aufgaben --- */
.todo-tags {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-top: 0.4rem;
}
.todo-tag-badge {
display: inline-block;
padding: 0.15rem 0.5rem;
background: var(--bg-tertiary, #e9ecef);
color: var(--text-secondary, #666);
font-size: 0.75rem;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.todo-tag-badge:hover {
background: var(--color-accent-light, #cce5ff);
color: var(--color-accent, #007bff);
}
.todo-tag-badge.active {
background: var(--color-accent, #007bff);
color: #fff;
}
/* --- Drag & Drop Styles --- */
.todo-drag-handle {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
color: var(--text-tertiary, #999);
cursor: grab;
opacity: 0.5;
transition: opacity 0.2s ease;
}
.todo-drag-handle:hover {
opacity: 1;
}
.todo-drag-handle:active {
cursor: grabbing;
}
/* Aufgabe während des Ziehens */
#todoList li.dragging {
opacity: 0.5;
background: var(--bg-secondary, #f5f5f5);
border: 2px dashed var(--color-accent, #007bff);
}
/* Drop-Ziel markieren */
#todoList li.drag-over {
border-top: 3px solid var(--color-accent, #007bff);
margin-top: -3px;
}
/* Drag-Handle nur bei Hover auf Aufgabe zeigen (optional) */
/*
#todoList li .todo-drag-handle {
visibility: hidden;
}
#todoList li:hover .todo-drag-handle {
visibility: visible;
}
*/
/* --- Modal Hint für Tags --- */
.todo-modal-hint {
font-size: 0.8rem;
color: var(--text-tertiary, #999);
margin: 0.5rem 0 1rem 0;
font-style: italic;
}
/* --- Responsive Anpassungen --- */
@media (max-width: 600px) {
.todo-filters {
flex-direction: column;
align-items: stretch;
}
.todo-filter-buttons {
justify-content: center;
}
.todo-priority-filter {
width: 100%;
}
.todo-priority-filter select {
width: 100%;
}
.todo-tag-filters {
justify-content: center;
}
}
Mehr aus JavaScript
Tutorials werden geladen...