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 speichern
  • JSON.stringify() / JSON.parse() – Objekte in Text umwandeln
  • Array.filter() / Array.sort() – Daten filtern und sortieren
  • DOM-Manipulation mit createElement() und textContent
  • 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.

    Wichtig

    localStorage kann nur Text speichern, keine JavaScript-Objekte oder Arrays. Deshalb brauchen wir JSON.stringify() und JSON.parse() zur Umwandlung.

    Sind meine Daten sicher?

    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:

    JavaScript
    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

    JavaScript
    // 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

    JavaScript
    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:

    JavaScript
    // 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:

    CSS
    #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:

    JavaScript
    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:

    JavaScript
    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
    }
    Warum ist das sicher?

    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)

    JavaScript
    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

    JavaScript
    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!

    Warum nicht innerHTML?

    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.

    Wann ist innerHTML trotzdem sicher?

    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 String
    • btn.innerHTML = '<svg>...</svg>';Sicher dein eigener SVG-Code
    • div.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:

    CSS
    .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:

    JavaScript
    // 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:

    JavaScript
    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:

    JavaScript
    // 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:

    JavaScript
    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:

    JavaScript
    // 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:

    JavaScript
    // 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:

    JavaScript
    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":

    JavaScript
    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:

    JavaScript
    // 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:

    JavaScript
    // 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;
    });
    Warum ID statt Index?

    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:

    JavaScript
    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:

    Das hast du gelernt
    • 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
    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
    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
    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...