// ====================== KANBAN BOARD APP ====================== // Single-Page Kanban Board mit Drag & Drop und localStorage Persistenz class KanbanBoard { constructor() { this.container = document.getElementById('kanbanContainer'); this.board = this.loadFromLocalStorage() || this.getDefaultBoard(); this.draggedCard = null; this.draggedFromColumn = null; this.colorPickerCard = null; this.initializeEventListeners(); this.render(); } /** * Standard-Board mit 3 Spalten */ getDefaultBoard() { return { columns: [ { id: this.generateId(), title: 'To Do', cards: [ { id: this.generateId(), text: 'Beispiel Aufgabe 1', color: null }, { id: this.generateId(), text: 'Beispiel Aufgabe 2', color: null } ] }, { id: this.generateId(), title: 'In Arbeit', cards: [ { id: this.generateId(), text: 'Aktuelle Aufgabe', color: null } ] }, { id: this.generateId(), title: 'Erledigt', cards: [ { id: this.generateId(), text: 'Abgeschlossene Aufgabe', color: null } ] } ] }; } /** * Event Listener initialisieren */ initializeEventListeners() { document.getElementById('addColumnBtn').addEventListener('click', () => this.addColumn()); document.getElementById('exportBtn').addEventListener('click', () => this.exportAsJSON()); document.getElementById('importBtn').addEventListener('click', () => this.triggerImport()); document.getElementById('resetBtn').addEventListener('click', () => this.resetBoard()); document.getElementById('importFileInput').addEventListener('change', (e) => this.importFromJSON(e)); // Close color picker wenn außerhalb geklickt wird document.addEventListener('click', (e) => { if (!e.target.closest('.color-picker') && !e.target.closest('.kanban-card')) { this.hideColorPicker(); } }); } /** * Spalten und Karten rendern */ render() { this.container.innerHTML = ''; this.board.columns.forEach(column => { const columnEl = document.createElement('div'); columnEl.className = 'kanban-column'; columnEl.id = `column-${column.id}`; columnEl.draggable = false; // Column Header const headerEl = document.createElement('div'); headerEl.className = 'kanban-column-header'; const titleEl = document.createElement('h2'); titleEl.className = 'kanban-column-title'; titleEl.textContent = `${column.title} (${column.cards.length})`; const deleteBtn = document.createElement('button'); deleteBtn.className = 'column-delete-btn'; deleteBtn.textContent = '✕'; deleteBtn.addEventListener('click', () => this.deleteColumn(column.id)); headerEl.appendChild(titleEl); headerEl.appendChild(deleteBtn); // Cards Container const cardsEl = document.createElement('div'); cardsEl.className = 'kanban-cards'; cardsEl.id = `cards-${column.id}`; // Drag & Drop Events für Spalte cardsEl.addEventListener('dragover', (e) => this.handleDragOver(e, column.id)); cardsEl.addEventListener('dragleave', (e) => this.handleDragLeave(e, column.id)); cardsEl.addEventListener('drop', (e) => this.handleDrop(e, column.id)); cardsEl.addEventListener('dragenter', (e) => this.handleDragEnter(e, column.id)); // Karten rendern column.cards.forEach(card => { const cardEl = this.createCardElement(card, column.id); cardsEl.appendChild(cardEl); }); // Add Card Button const addCardBtn = document.createElement('button'); addCardBtn.className = 'add-card-btn'; addCardBtn.textContent = '+ Neue Karte'; addCardBtn.addEventListener('click', () => this.addCard(column.id)); cardsEl.appendChild(addCardBtn); columnEl.appendChild(headerEl); columnEl.appendChild(cardsEl); this.container.appendChild(columnEl); }); this.save(); } /** * Karten-Element erstellen */ createCardElement(card, columnId) { const cardEl = document.createElement('div'); cardEl.className = 'kanban-card'; cardEl.id = `card-${card.id}`; cardEl.draggable = true; // Drag Events cardEl.addEventListener('dragstart', (e) => this.handleDragStart(e, card, columnId)); cardEl.addEventListener('dragend', (e) => this.handleDragEnd(e)); // Card Content const contentEl = document.createElement('div'); contentEl.className = 'card-content'; const textEl = document.createElement('p'); textEl.className = 'card-text'; textEl.textContent = card.text; textEl.contentEditable = 'true'; textEl.addEventListener('blur', () => { card.text = textEl.textContent; this.save(); }); contentEl.appendChild(textEl); // Label anzeigen, falls vorhanden if (card.color) { const labelEl = document.createElement('div'); labelEl.className = `card-label label-${card.color}`; labelEl.textContent = this.getColorName(card.color); contentEl.appendChild(labelEl); } // Rechtsklick für Farben (Bonus) cardEl.addEventListener('contextmenu', (e) => { e.preventDefault(); this.showColorPicker(e, card, columnId); }); // Card Actions const actionsEl = document.createElement('div'); actionsEl.className = 'card-actions'; const deleteBtn = document.createElement('button'); deleteBtn.className = 'card-delete-btn'; deleteBtn.textContent = '✕'; deleteBtn.addEventListener('click', () => this.deleteCard(columnId, card.id)); actionsEl.appendChild(deleteBtn); cardEl.appendChild(contentEl); cardEl.appendChild(actionsEl); return cardEl; } /** * Spalte hinzufügen */ addColumn() { const title = prompt('Spaltenname eingeben:'); if (!title || title.trim() === '') return; const newColumn = { id: this.generateId(), title: title.trim(), cards: [] }; this.board.columns.push(newColumn); this.render(); } /** * Spalte löschen */ deleteColumn(columnId) { if (confirm('Spalte und alle Karten löschen?')) { this.board.columns = this.board.columns.filter(col => col.id !== columnId); this.render(); } } /** * Karte hinzufügen */ addCard(columnId) { const column = this.board.columns.find(col => col.id === columnId); if (!column) return; const newCard = { id: this.generateId(), text: 'Neue Aufgabe...', color: null }; column.cards.push(newCard); this.render(); } /** * Karte löschen */ deleteCard(columnId, cardId) { const column = this.board.columns.find(col => col.id === columnId); if (!column) return; column.cards = column.cards.filter(card => card.id !== cardId); this.render(); } /** * ====================== DRAG & DROP ====================== */ handleDragStart(e, card, columnId) { this.draggedCard = card; this.draggedFromColumn = columnId; const cardEl = document.getElementById(`card-${card.id}`); cardEl.classList.add('dragging'); } handleDragEnd(e) { document.querySelectorAll('.kanban-card').forEach(card => { card.classList.remove('dragging'); card.classList.remove('drag-over-card'); }); document.querySelectorAll('.kanban-column').forEach(col => { col.classList.remove('drag-over'); }); } handleDragOver(e, columnId) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const cardsContainer = document.getElementById(`cards-${columnId}`); const afterElement = this.getDragAfterElement(cardsContainer, e.clientY); if (afterElement == null) { // Vor den Add Button einfügen cardsContainer.insertBefore(this.draggedCard, cardsContainer.lastElementChild); } else { cardsContainer.insertBefore(this.draggedCard, afterElement); } } handleDragEnter(e, columnId) { const columnEl = document.getElementById(`column-${columnId}`); columnEl.classList.add('drag-over'); } handleDragLeave(e, columnId) { const columnEl = document.getElementById(`column-${columnId}`); const cardsEl = document.getElementById(`cards-${columnId}`); // Nur entfernen wenn wirklich die Spalte verlassen wird if (e.target === cardsEl || e.target.classList.contains('kanban-cards')) { columnEl.classList.remove('drag-over'); } } handleDrop(e, columnId) { e.preventDefault(); const columnEl = document.getElementById(`column-${columnId}`); columnEl.classList.remove('drag-over'); if (!this.draggedCard) return; // Alte Spalte aktualisieren const oldColumn = this.board.columns.find(col => col.id === this.draggedFromColumn); if (oldColumn) { oldColumn.cards = oldColumn.cards.filter(card => card.id !== this.draggedCard.id); } // Neue Spalte aktualisieren const newColumn = this.board.columns.find(col => col.id === columnId); if (newColumn) { // Prüfen ob die Karte bereits in der neuen Spalte ist (aus derselben Spalte kommend) if (!newColumn.cards.find(card => card.id === this.draggedCard.id)) { newColumn.cards.push(this.draggedCard); } } this.draggedCard = null; this.draggedFromColumn = null; this.render(); } /** * Hilfsfunktion um Position des draggable Elements zu bestimmen */ getDragAfterElement(container, y) { const draggableElements = [...container.querySelectorAll('.kanban-card:not(.dragging)')]; return draggableElements.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; if (offset < 0 && offset > closest.offset) { return { offset: offset, element: child }; } else { return closest; } }, { offset: Number.NEGATIVE_INFINITY }).element; } /** * ====================== FARBEN (BONUS) ====================== */ showColorPicker(e, card, columnId) { this.hideColorPicker(); const picker = document.createElement('div'); picker.className = 'color-picker'; picker.style.left = e.clientX + 'px'; picker.style.top = e.clientY + 'px'; picker.id = 'colorPickerMenu'; const title = document.createElement('div'); title.className = 'color-picker-title'; title.textContent = 'Farbe wählen:'; const options = document.createElement('div'); options.className = 'color-options'; const colors = ['red', 'blue', 'yellow', 'green', 'purple']; colors.forEach(color => { const option = document.createElement('div'); option.className = `color-option label-${color}`; if (card.color === color) option.classList.add('active'); option.addEventListener('click', () => { card.color = card.color === color ? null : color; const column = this.board.columns.find(col => col.id === columnId); const cardToUpdate = column.cards.find(c => c.id === card.id); if (cardToUpdate) cardToUpdate.color = card.color; this.save(); this.render(); this.hideColorPicker(); }); options.appendChild(option); }); picker.appendChild(title); picker.appendChild(options); document.body.appendChild(picker); } hideColorPicker() { const picker = document.getElementById('colorPickerMenu'); if (picker) picker.remove(); } getColorName(color) { const names = { 'red': '🔴', 'blue': '🔵', 'yellow': '🟡', 'green': '🟢', 'purple': '🟣' }; return names[color] || ''; } /** * ====================== PERSISTENZ ====================== */ save() { localStorage.setItem('kanbanBoard', JSON.stringify(this.board)); } loadFromLocalStorage() { const stored = localStorage.getItem('kanbanBoard'); return stored ? JSON.parse(stored) : null; } /** * ====================== EXPORT/IMPORT ====================== */ exportAsJSON() { const dataStr = JSON.stringify(this.board, null, 2); const blob = new Blob([dataStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `kanban-board-${new Date().toISOString().split('T')[0]}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } triggerImport() { document.getElementById('importFileInput').click(); } importFromJSON(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const imported = JSON.parse(event.target.result); if (imported.columns && Array.isArray(imported.columns)) { this.board = imported; this.save(); this.render(); alert('Board erfolgreich importiert!'); } else { alert('Ungültiges JSON-Format!'); } } catch (error) { alert('Fehler beim Import: ' + error.message); } }; reader.readAsText(file); } resetBoard() { if (confirm('Möchtest du das Board wirklich zurücksetzen?')) { this.board = this.getDefaultBoard(); this.save(); this.render(); } } /** * Utility: Eindeutige ID generieren */ generateId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } } // ====================== APP STARTEN ====================== document.addEventListener('DOMContentLoaded', () => { new KanbanBoard(); });