/** * Mindmap-Initialisierer * Lädt und initialisiert die Mindmap-Visualisierung */ // Warte bis DOM geladen ist document.addEventListener('DOMContentLoaded', function() { // Prüfe, ob wir auf der Mindmap-Seite sind const cyContainer = document.getElementById('cy'); if (!cyContainer) { console.log('Kein Mindmap-Container gefunden, überspringe Initialisierung.'); return; } console.log('Initialisiere Mindmap-Visualisierung...'); // Prüfe, ob Cytoscape.js verfügbar ist if (typeof cytoscape === 'undefined') { loadScript('/static/js/cytoscape.min.js', initMindmap); } else { initMindmap(); } }); /** * Lädt ein Script dynamisch * @param {string} src - Quelldatei * @param {Function} callback - Callback nach dem Laden */ function loadScript(src, callback) { const script = document.createElement('script'); script.src = src; script.onload = callback; document.head.appendChild(script); } /** * Initialisiert die Mindmap-Visualisierung */ function initMindmap() { const cyContainer = document.getElementById('cy'); const fitBtn = document.getElementById('fit-btn'); const resetBtn = document.getElementById('reset-btn'); const toggleLabelsBtn = document.getElementById('toggle-labels-btn'); const nodeInfoPanel = document.getElementById('node-info-panel'); const nodeDescription = document.getElementById('node-description'); const connectedNodes = document.getElementById('connected-nodes'); let labelsVisible = true; let selectedNode = null; // Erstelle Cytoscape-Instanz const cy = cytoscape({ container: cyContainer, style: getDefaultStyles(), layout: { name: 'cose', animate: true, animationDuration: 800, nodeDimensionsIncludeLabels: true, padding: 50, spacingFactor: 1.2, randomize: true, componentSpacing: 100, nodeRepulsion: 8000, edgeElasticity: 100, nestingFactor: 1.2, gravity: 80 }, wheelSensitivity: 0.3, }); // Daten vom Server laden loadMindmapData(cy); // Event-Handler zuweisen setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes); } /** * Lädt die Mindmap-Daten vom Server * @param {Object} cy - Cytoscape-Instanz */ function loadMindmapData(cy) { fetch('/api/mindmap') .then(response => { if (!response.ok) { throw new Error(`HTTP Fehler: ${response.status}`); } return response.json(); }) .then(data => { if (!data.nodes || data.nodes.length === 0) { console.log('Keine Daten gefunden, versuche Refresh-API...'); return fetch('/api/refresh-mindmap') .then(response => { if (!response.ok) { throw new Error(`HTTP Fehler beim Refresh: ${response.status}`); } return response.json(); }); } return data; }) .then(data => { console.log('Mindmap-Daten geladen:', data); // Cytoscape-Elemente vorbereiten const elements = []; // Prüfen, ob "Wissen"-Knoten existiert let rootNode = data.nodes.find(node => node.name === "Wissen"); // Wenn nicht, Root-Knoten hinzufügen if (!rootNode) { rootNode = { id: 'root', name: 'Wissen', description: 'Zentrale Wissensbasis', color_code: '#4299E1' }; data.nodes.unshift(rootNode); } // Knoten hinzufügen data.nodes.forEach(node => { elements.push({ group: 'nodes', data: { id: node.id.toString(), name: node.name, description: node.description || '', color: node.color_code || '#8B5CF6', isRoot: node.name === 'Wissen' } }); }); // Kanten hinzufügen, wenn vorhanden if (data.edges && data.edges.length > 0) { data.edges.forEach(edge => { elements.push({ group: 'edges', data: { id: `${edge.source}-${edge.target}`, source: edge.source.toString(), target: edge.target.toString() } }); }); } else { // Wenn keine Kanten definiert sind, verbinde alle Knoten mit dem Root-Knoten const rootId = rootNode.id.toString(); data.nodes.forEach(node => { if (node.id.toString() !== rootId) { elements.push({ group: 'edges', data: { id: `${rootId}-${node.id}`, source: rootId, target: node.id.toString() } }); } }); } // Elemente zu Cytoscape hinzufügen cy.elements().remove(); cy.add(elements); // Layout anwenden cy.layout({ name: 'cose', animate: true, animationDuration: 800, nodeDimensionsIncludeLabels: true, padding: 50, spacingFactor: 1.5, randomize: false, fit: true }).run(); // Nach dem Laden Event auslösen document.dispatchEvent(new CustomEvent('mindmap-loaded')); }) .catch(error => { console.error('Fehler beim Laden der Mindmap-Daten:', error); // Fallback mit Standard-Daten const fallbackData = { nodes: [ { id: 1, name: 'Wissen', description: 'Zentrale Wissensbasis', color_code: '#4299E1' }, { id: 2, name: 'Philosophie', description: 'Philosophisches Denken', color_code: '#9F7AEA' }, { id: 3, name: 'Wissenschaft', description: 'Wissenschaftliche Erkenntnisse', color_code: '#48BB78' }, { id: 4, name: 'Technologie', description: 'Technologische Entwicklungen', color_code: '#ED8936' }, { id: 5, name: 'Künste', description: 'Künstlerische Ausdrucksformen', color_code: '#ED64A6' } ], edges: [ { source: 1, target: 2 }, { source: 1, target: 3 }, { source: 1, target: 4 }, { source: 1, target: 5 } ] }; const fallbackElements = []; // Knoten hinzufügen fallbackData.nodes.forEach(node => { fallbackElements.push({ group: 'nodes', data: { id: node.id.toString(), name: node.name, description: node.description || '', color: node.color_code || '#8B5CF6', isRoot: node.name === 'Wissen' } }); }); // Kanten hinzufügen fallbackData.edges.forEach(edge => { fallbackElements.push({ group: 'edges', data: { id: `${edge.source}-${edge.target}`, source: edge.source.toString(), target: edge.target.toString() } }); }); // Elemente zu Cytoscape hinzufügen cy.elements().remove(); cy.add(fallbackElements); // Layout anwenden cy.layout({ name: 'cose', animate: true }).run(); }); } /** * Richtet Event-Listener für die Mindmap ein * @param {Object} cy - Cytoscape-Instanz * @param {HTMLElement} fitBtn - Fit-Button * @param {HTMLElement} resetBtn - Reset-Button * @param {HTMLElement} toggleLabelsBtn - Toggle-Labels-Button * @param {HTMLElement} nodeInfoPanel - Node-Info-Panel * @param {HTMLElement} nodeDescription - Node-Description * @param {HTMLElement} connectedNodes - Connected-Nodes-Container */ function setupEventListeners(cy, fitBtn, resetBtn, toggleLabelsBtn, nodeInfoPanel, nodeDescription, connectedNodes) { let labelsVisible = true; // Fit-Button if (fitBtn) { fitBtn.addEventListener('click', function() { cy.fit(); }); } // Reset-Button if (resetBtn) { resetBtn.addEventListener('click', function() { cy.layout({ name: 'cose', animate: true }).run(); }); } // Toggle-Labels-Button if (toggleLabelsBtn) { toggleLabelsBtn.addEventListener('click', function() { labelsVisible = !labelsVisible; cy.style() .selector('node') .style({ 'text-opacity': labelsVisible ? 1 : 0 }) .update(); }); } // Knoten-Klick cy.on('tap', 'node', function(evt) { const node = evt.target; // Zuvor ausgewählten Knoten zurücksetzen cy.nodes().removeClass('selected'); // Neuen Knoten auswählen node.addClass('selected'); if (nodeInfoPanel && nodeDescription && connectedNodes) { // Info-Panel aktualisieren nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.'; // Verbundene Knoten anzeigen connectedNodes.innerHTML = ''; // Verbundene Knoten sammeln const connectedNodesList = node.neighborhood('node'); if (connectedNodesList.length > 0) { connectedNodesList.forEach(connectedNode => { // Nicht den ausgewählten Knoten selbst anzeigen if (connectedNode.id() !== node.id()) { const nodeLink = document.createElement('span'); nodeLink.className = 'node-link'; nodeLink.textContent = connectedNode.data('name'); nodeLink.style.backgroundColor = connectedNode.data('color'); // Klick-Ereignis, um zu diesem Knoten zu wechseln nodeLink.addEventListener('click', function() { connectedNode.select(); cy.animate({ center: { eles: connectedNode }, duration: 500, easing: 'ease-in-out-cubic' }); }); connectedNodes.appendChild(nodeLink); } }); } else { connectedNodes.innerHTML = 'Keine verbundenen Knoten'; } // Panel anzeigen nodeInfoPanel.classList.add('visible'); } }); // Hintergrund-Klick cy.on('tap', function(evt) { if (evt.target === cy) { // Klick auf den Hintergrund cy.nodes().removeClass('selected'); // Info-Panel verstecken if (nodeInfoPanel) { nodeInfoPanel.classList.remove('visible'); } } }); // Dark Mode-Änderungen document.addEventListener('darkModeToggled', function(event) { const isDark = event.detail.isDark; cy.style(getDefaultStyles(isDark)); }); } /** * Liefert die Standard-Stile für die Mindmap * @param {boolean} darkMode - Ob der Dark Mode aktiv ist * @returns {Array} Array von Cytoscape-Stilen */ function getDefaultStyles(darkMode = document.documentElement.classList.contains('dark')) { return [ { selector: 'node', style: { 'background-color': 'data(color)', 'label': 'data(name)', 'width': 40, 'height': 40, 'font-size': 12, 'text-valign': 'bottom', 'text-halign': 'center', 'text-margin-y': 8, 'color': darkMode ? '#f1f5f9' : '#334155', 'text-background-color': darkMode ? 'rgba(30, 41, 59, 0.8)' : 'rgba(241, 245, 249, 0.8)', 'text-background-opacity': 0.8, 'text-background-padding': '2px', 'text-background-shape': 'roundrectangle', 'text-wrap': 'ellipsis', 'text-max-width': '100px' } }, { selector: 'node[?isRoot]', style: { 'width': 60, 'height': 60, 'font-size': 14, 'font-weight': 'bold', 'text-background-opacity': 0.9, 'text-background-color': '#4299E1' } }, { selector: 'edge', style: { 'width': 2, 'line-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)', 'target-arrow-color': darkMode ? 'rgba(255, 255, 255, 0.15)' : 'rgba(30, 41, 59, 0.15)', 'curve-style': 'bezier', 'target-arrow-shape': 'triangle' } }, { selector: 'node.selected', style: { 'background-color': 'data(color)', 'border-width': 3, 'border-color': '#8b5cf6', 'width': 50, 'height': 50, 'font-size': 14, 'font-weight': 'bold', 'text-background-color': '#8b5cf6', 'text-background-opacity': 0.9 } } ]; } class MindMap { constructor(selector, options = {}) { // Standardoptionen festlegen this.options = Object.assign({ // Standard-Basisoptionen editable: false, // Ist die Mindmap editierbar? isUserLoggedIn: false, // Ist der Benutzer angemeldet? isPublicMap: true, // Handelt es sich um die öffentliche Mindmap? initialZoom: 1, // Anfängliche Zoom-Stufe fitViewOnInit: true, // Passt die Ansicht automatisch an minZoom: 0.2, // Minimale Zoom-Stufe maxZoom: 3, // Maximale Zoom-Stufe // Farbpalette für verschiedene Elemente colorPalette: { node: { default: '#8b5cf6', // Standard-Knotenfarbe selected: '#7c3aed', // Farbe für ausgewählte Knoten hover: '#6d28d9' // Farbe für Hover-Effekt }, edge: { default: '#8b5cf6', // Standard-Kantenfarbe selected: '#7c3aed', // Farbe für ausgewählte Kanten hover: '#6d28d9' // Farbe für Hover-Effekt }, text: { default: '#f3f4f6', // Standard-Textfarbe dark: '#1f2937', // Dunkle Textfarbe (für helle Hintergründe) light: '#f9fafb' // Helle Textfarbe (für dunkle Hintergründe) }, background: { dark: '#111827', // Dunkler Hintergrund light: '#f9fafb' // Heller Hintergrund } }, // Anpassbare Funktionen callbacks: { onNodeClick: null, // Callback für Knotenklick onEdgeClick: null, // Callback für Kantenklick onViewportChange: null, // Callback für Änderung der Ansicht onSelectionChange: null, // Callback für Änderung der Auswahl onLoad: null // Callback nach dem Laden der Mindmap } }, options); // Container-Element this.container = document.querySelector(selector); if (!this.container) { console.error(`Container mit Selektor '${selector}' nicht gefunden`); return; } // Prüfen, ob der Benutzer angemeldet ist this.isUserLoggedIn = document.body.classList.contains('user-logged-in') || this.options.isUserLoggedIn; // Wenn der Benutzer angemeldet ist und es sich um die öffentliche Mindmap handelt, // wird die Editierbarkeit aktiviert if (this.isUserLoggedIn && this.options.isPublicMap) { this.options.editable = true; } // Initialisiere Cytoscape this.initCytoscape(); // Füge Bearbeiten-Funktionalität hinzu, wenn editierbar if (this.options.editable) { this.initEditableMode(); } // Lade Daten basierend auf dem Typ der Mindmap if (this.options.isPublicMap) { this.loadPublicMindmap(); } else if (this.options.userMindmapId) { this.loadUserMindmap(this.options.userMindmapId); } // Initialisiere UI-Elemente this.initUI(); // Event-Listener-Initialisierung this.initEventListeners(); // Flash-Message-System this.flashContainer = null; this.initFlashMessages(); } // ... existing code ... // Initialisiert die Bearbeitungsmodi für die Mindmap initEditableMode() { // Toolbar für Bearbeitungsmodus hinzufügen this.addEditToolbar(); // Kontextmenü-Funktionalität für Knoten this.cy.on('cxttap', 'node', (evt) => { const node = evt.target; const position = evt.renderedPosition; // Zeige Kontextmenü this.showNodeContextMenu(node, position); }); // Doppelklick-Funktion für das Hinzufügen neuer Knoten this.cy.on('dblclick', (evt) => { // Nur auf Hintergrund reagieren, nicht auf Knoten if (evt.target === this.cy) { const position = evt.position; this.showAddNodeDialog(position); } }); // Kontextmenü-Schließen bei Klick außerhalb document.addEventListener('click', (e) => { const contextMenu = document.getElementById('context-menu'); if (contextMenu && !contextMenu.contains(e.target)) { contextMenu.remove(); } }); } // Zeigt ein Kontextmenü für einen Knoten an showNodeContextMenu(node, position) { // Entferne vorhandenes Kontextmenü const existingMenu = document.getElementById('context-menu'); if (existingMenu) existingMenu.remove(); // Erstelle neues Kontextmenü const contextMenu = document.createElement('div'); contextMenu.id = 'context-menu'; contextMenu.style.position = 'absolute'; contextMenu.style.left = `${position.x}px`; contextMenu.style.top = `${position.y}px`; contextMenu.style.zIndex = '1000'; // Menü-Elemente const menuItems = [ { label: 'Gedanken anzeigen', icon: 'fas fa-brain', action: () => this.showThoughtsForNode(node) }, { label: 'Neuen Gedanken erstellen', icon: 'fas fa-plus-circle', action: () => this.createThoughtForNode(node) }, { label: 'Notiz hinzufügen', icon: 'fas fa-sticky-note', action: () => this.addNoteToNode(node) }, { label: 'Knoten bearbeiten', icon: 'fas fa-edit', action: () => this.editNode(node) }, { label: 'Verbindung erstellen', icon: 'fas fa-link', action: () => this.startEdgeCreation(node) }, { label: 'Aus Mindmap entfernen', icon: 'fas fa-trash-alt', action: () => this.removeNodeFromMap(node) } ]; // Menü erstellen menuItems.forEach(item => { const menuItem = document.createElement('div'); menuItem.className = 'menu-item'; menuItem.innerHTML = ` ${item.label}`; menuItem.addEventListener('click', () => { item.action(); contextMenu.remove(); }); contextMenu.appendChild(menuItem); }); // Anhängen an Body (außerhalb des Cytoscape-Containers) document.body.appendChild(contextMenu); // Stellen Sie sicher, dass das Kontextmenü vollständig sichtbar ist const menuRect = contextMenu.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; if (menuRect.right > windowWidth) { contextMenu.style.left = `${position.x - menuRect.width}px`; } if (menuRect.bottom > windowHeight) { contextMenu.style.top = `${position.y - menuRect.height}px`; } } // Fügt die Edit-Toolbar zur Mindmap hinzu addEditToolbar() { const toolbar = document.querySelector('.mindmap-toolbar'); if (!toolbar) return; // Trennlinie const separator = document.createElement('div'); separator.className = 'border-l h-6 mx-2 opacity-20'; toolbar.appendChild(separator); // Bearbeiten-Buttons const editButtons = [ { id: 'add-node-btn', icon: 'fa-plus', text: 'Knoten hinzufügen', action: () => this.showAddNodeDialog() }, { id: 'save-layout-btn', icon: 'fa-save', text: 'Layout speichern', action: () => this.saveCurrentLayout() } ]; editButtons.forEach(btn => { const button = document.createElement('button'); button.id = btn.id; button.className = 'mindmap-action-btn edit-btn'; button.innerHTML = `${btn.text}`; button.addEventListener('click', btn.action); toolbar.appendChild(button); }); // Animation für die Edit-Buttons const editBtns = document.querySelectorAll('.edit-btn'); editBtns.forEach(btn => { btn.style.opacity = '0'; btn.style.transform = 'translateY(10px)'; setTimeout(() => { btn.style.transition = 'all 0.3s ease'; btn.style.opacity = '1'; btn.style.transform = 'translateY(0)'; }, 100); }); } // Zeigt den Dialog zum Hinzufügen eines neuen Knotens showAddNodeDialog(position = null) { // Entferne bestehende Dialoge const existingDialog = document.getElementById('add-node-dialog'); if (existingDialog) existingDialog.remove(); // Erstelle Dialog const dialog = document.createElement('div'); dialog.id = 'add-node-dialog'; dialog.className = 'fixed inset-0 flex items-center justify-center z-50'; dialog.innerHTML = `

Neuen Knoten hinzufügen

`; // Füge Dialog zum Body hinzu document.body.appendChild(dialog); // Animation für den Dialog const dialogContent = dialog.querySelector('.bg-slate-800'); dialogContent.style.opacity = '0'; dialogContent.style.transform = 'scale(0.9)'; setTimeout(() => { dialogContent.style.transition = 'all 0.3s ease'; dialogContent.style.opacity = '1'; dialogContent.style.transform = 'scale(1)'; }, 10); // Event-Listener für Abbrechen document.getElementById('cancel-add-node').addEventListener('click', () => { // Animation für das Schließen dialogContent.style.opacity = '0'; dialogContent.style.transform = 'scale(0.9)'; setTimeout(() => dialog.remove(), 300); }); // Event-Listener für Overlay-Klick dialog.querySelector('.fixed.inset-0.bg-black').addEventListener('click', () => { // Animation für das Schließen dialogContent.style.opacity = '0'; dialogContent.style.transform = 'scale(0.9)'; setTimeout(() => dialog.remove(), 300); }); // Event-Listener für Formular document.getElementById('add-node-form').addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('node-name').value; const description = document.getElementById('node-description').value; const color = document.getElementById('node-color').value; if (!name.trim()) { this.showFlash('Bitte geben Sie einen Namen für den Knoten ein', 'error'); return; } // Knoten hinzufügen this.addNodeToMindmap({ name, description, color_code: color, position }); // Dialog schließen dialogContent.style.opacity = '0'; dialogContent.style.transform = 'scale(0.9)'; setTimeout(() => dialog.remove(), 300); }); // Fokus auf das Name-Feld setTimeout(() => document.getElementById('node-name').focus(), 100); } // Fügt einen neuen Knoten zur Mindmap hinzu addNodeToMindmap(nodeData) { // API-Anfrage vorbereiten const data = { name: nodeData.name, description: nodeData.description || '', color_code: nodeData.color_code || '#8b5cf6' }; // Position hinzufügen, falls vorhanden if (nodeData.position) { data.x_position = nodeData.position.x; data.y_position = nodeData.position.y; } // API-Anfrage zum Hinzufügen des Knotens fetch('/api/mindmap/public/add_node', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => { if (!response.ok) { throw new Error('Netzwerkantwort war nicht ok'); } return response.json(); }) .then(data => { if (data.success) { // Hinzufügen des neuen Knotens zum Graphen this.cy.add({ group: 'nodes', data: { id: `node_${data.node_id}`, name: nodeData.name, description: nodeData.description || '', color: nodeData.color_code }, position: nodeData.position || { x: 0, y: 0 } }); // Flash-Nachricht anzeigen this.showFlash('Knoten erfolgreich hinzugefügt', 'success'); // Ansicht anpassen this.cy.fit(); } else { this.showFlash('Fehler beim Hinzufügen des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error'); } }) .catch(error => { console.error('Fehler beim Hinzufügen des Knotens:', error); this.showFlash('Fehler beim Hinzufügen des Knotens', 'error'); }); } // Entfernt einen Knoten aus der Mindmap removeNodeFromMap(node) { if (!confirm('Möchten Sie diesen Knoten wirklich aus der Mindmap entfernen?')) { return; } const nodeId = node.id().split('_')[1]; // "node_123" => "123" // API-Anfrage zum Entfernen des Knotens fetch(`/api/mindmap/public/remove_node/${nodeId}`, { method: 'DELETE' }) .then(response => { if (!response.ok) { throw new Error('Netzwerkantwort war nicht ok'); } return response.json(); }) .then(data => { if (data.success) { // Knoten aus dem Graphen entfernen this.cy.remove(node); // Flash-Nachricht anzeigen this.showFlash('Knoten erfolgreich entfernt', 'success'); } else { this.showFlash('Fehler beim Entfernen des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error'); } }) .catch(error => { console.error('Fehler beim Entfernen des Knotens:', error); this.showFlash('Fehler beim Entfernen des Knotens', 'error'); }); } // Speichert das aktuelle Layout der Mindmap saveCurrentLayout() { // Sammle alle Knotenpositionen const nodePositions = []; this.cy.nodes().forEach(node => { const idParts = node.id().split('_'); if (idParts.length === 2 && idParts[0] === 'node') { nodePositions.push({ node_id: parseInt(idParts[1]), x_position: node.position('x'), y_position: node.position('y') }); } }); // API-Anfrage zum Speichern des Layouts fetch('/api/mindmap/public/update_layout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ positions: nodePositions }) }) .then(response => { if (!response.ok) { throw new Error('Netzwerkantwort war nicht ok'); } return response.json(); }) .then(data => { if (data.success) { this.showFlash('Layout erfolgreich gespeichert', 'success'); } else { this.showFlash('Fehler beim Speichern des Layouts: ' + (data.error || 'Unbekannter Fehler'), 'error'); } }) .catch(error => { console.error('Fehler beim Speichern des Layouts:', error); this.showFlash('Fehler beim Speichern des Layouts', 'error'); }); } // Zeigt den Dialog zum Bearbeiten eines Knotens editNode(node) { // Entferne bestehende Dialoge const existingDialog = document.getElementById('edit-node-dialog'); if (existingDialog) existingDialog.remove(); // Knotendaten holen const nodeData = node.data(); // Erstelle Dialog const dialog = document.createElement('div'); dialog.id = 'edit-node-dialog'; dialog.className = 'fixed inset-0 flex items-center justify-center z-50'; dialog.innerHTML = `

Knoten bearbeiten

`; // Füge Dialog zum Body hinzu document.body.appendChild(dialog); // Animation für den Dialog const dialogContent = dialog.querySelector('.bg-slate-800'); dialogContent.style.opacity = '0'; dialogContent.style.transform = 'scale(0.9)'; setTimeout(() => { dialogContent.style.transition = 'all 0.3s ease'; dialogContent.style.opacity = '1'; dialogContent.style.transform = 'scale(1)'; }, 10); // Event-Listener für Abbrechen document.getElementById('cancel-edit-node').addEventListener('click', () => { // Animation für das Schließen dialogContent.style.opacity = '0'; dialogContent.style.transform = 'scale(0.9)'; setTimeout(() => dialog.remove(), 300); }); // Event-Listener für Overlay-Klick dialog.querySelector('.fixed.inset-0.bg-black').addEventListener('click', () => { // Animation für das Schließen dialogContent.style.opacity = '0'; dialogContent.style.transform = 'scale(0.9)'; setTimeout(() => dialog.remove(), 300); }); // Event-Listener für Formular document.getElementById('edit-node-form').addEventListener('submit', (e) => { e.preventDefault(); const name = document.getElementById('node-name').value; const description = document.getElementById('node-description').value; const color = document.getElementById('node-color').value; if (!name.trim()) { this.showFlash('Bitte geben Sie einen Namen für den Knoten ein', 'error'); return; } // Knoten aktualisieren this.updateNode(node, { name, description, color_code: color }); // Dialog schließen dialogContent.style.opacity = '0'; dialogContent.style.transform = 'scale(0.9)'; setTimeout(() => dialog.remove(), 300); }); // Fokus auf das Name-Feld setTimeout(() => document.getElementById('node-name').focus(), 100); } // Aktualisiert einen Knoten in der Mindmap updateNode(node, nodeData) { const nodeId = node.id().split('_')[1]; // "node_123" => "123" // API-Anfrage zum Aktualisieren des Knotens fetch(`/api/mindmap/public/update_node/${nodeId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: nodeData.name, description: nodeData.description, color_code: nodeData.color_code }) }) .then(response => { if (!response.ok) { throw new Error('Netzwerkantwort war nicht ok'); } return response.json(); }) .then(data => { if (data.success) { // Aktualisiere Knotendaten im Graph node.data('name', nodeData.name); node.data('description', nodeData.description); node.data('color', nodeData.color_code); // Aktualisiere Darstellung this.cy.style() .selector(`#${node.id()}`) .style({ 'background-color': nodeData.color_code }) .update(); // Flash-Nachricht anzeigen this.showFlash('Knoten erfolgreich aktualisiert', 'success'); } else { this.showFlash('Fehler beim Aktualisieren des Knotens: ' + (data.error || 'Unbekannter Fehler'), 'error'); } }) .catch(error => { console.error('Fehler beim Aktualisieren des Knotens:', error); this.showFlash('Fehler beim Aktualisieren des Knotens', 'error'); }); } // Fügt eine Notiz zu einem Knoten hinzu addNoteToNode(node) { // Implementierung für das Hinzufügen von Notizen // Ähnlich wie bei editNode, aber mit anderen Feldern } // Startet den Prozess zum Erstellen einer Verbindung startEdgeCreation(node) { // Implementierung für das Erstellen von Verbindungen } // Zeigt Gedanken für einen Knoten an showThoughtsForNode(node) { const nodeId = node.id().split('_')[1]; // "node_123" => "123" // Wir verwenden die fetchThoughtsForNode-Methode, die wir bereits implementiert haben this.fetchThoughtsForNode(nodeId) .then(thoughts => { // Nur fortfahren, wenn wir tatsächlich Gedanken haben if (thoughts.length === 0) { this.showFlash('Keine Gedanken für diesen Knoten gefunden', 'info'); return; } // Erstelle einen Gedanken-Viewer, wenn er nicht existiert let thoughtsViewer = document.getElementById('thoughts-viewer'); if (!thoughtsViewer) { thoughtsViewer = document.createElement('div'); thoughtsViewer.id = 'thoughts-viewer'; thoughtsViewer.className = 'fixed inset-0 flex items-center justify-center z-50'; thoughtsViewer.innerHTML = `

Gedanken zu:

`; document.body.appendChild(thoughtsViewer); // Event-Listener für Schließen-Button document.getElementById('close-thoughts').addEventListener('click', () => { // Animation für das Schließen const content = thoughtsViewer.querySelector('.thoughts-content'); const backdrop = thoughtsViewer.querySelector('.thoughts-backdrop'); content.style.transform = 'scale(0.9)'; content.style.opacity = '0'; backdrop.style.opacity = '0'; setTimeout(() => thoughtsViewer.remove(), 300); }); // Event-Listener für Backdrop-Klick thoughtsViewer.querySelector('.thoughts-backdrop').addEventListener('click', () => { document.getElementById('close-thoughts').click(); }); } // Aktualisiere den Titel mit dem Knotennamen thoughtsViewer.querySelector('.node-name').textContent = node.data('name'); // Container für die Gedanken const thoughtsContainer = thoughtsViewer.querySelector('.thoughts-container'); thoughtsContainer.innerHTML = ''; // Gedanken rendern this.renderThoughts(thoughts, thoughtsContainer); // Animation für das Öffnen const content = thoughtsViewer.querySelector('.thoughts-content'); const backdrop = thoughtsViewer.querySelector('.thoughts-backdrop'); content.style.transform = 'scale(0.9)'; content.style.opacity = '0'; backdrop.style.opacity = '0'; setTimeout(() => { content.style.transition = 'all 0.3s ease'; backdrop.style.transition = 'opacity 0.3s ease'; content.style.transform = 'scale(1)'; content.style.opacity = '1'; backdrop.style.opacity = '1'; }, 10); }); } // Rendert die Gedanken in der UI mit Animationen renderThoughts(thoughts, container) { if (!container) return; container.innerHTML = ''; // Animation delay counter let delay = 0; thoughts.forEach(thought => { const thoughtCard = document.createElement('div'); thoughtCard.className = 'thought-card mb-4 bg-slate-700/50 rounded-lg overflow-hidden border border-slate-600/50 transition-all duration-300 hover:border-purple-500/30'; thoughtCard.setAttribute('data-id', thought.id); thoughtCard.style.opacity = '0'; thoughtCard.style.transform = 'translateY(20px)'; const cardColor = thought.color_code || this.colorPalette.default; thoughtCard.innerHTML = `

${thought.title}

${new Date(thought.created_at).toLocaleDateString('de-DE')} ${thought.author ? `${thought.author.username}` : ''}

${thought.abstract || thought.content.substring(0, 150) + '...'}

`; // Animation-Effekt mit Verzögerung für jede Karte setTimeout(() => { thoughtCard.style.transition = 'all 0.5s ease'; thoughtCard.style.opacity = '1'; thoughtCard.style.transform = 'translateY(0)'; }, delay); delay += 100; // Jede Karte erscheint mit 100ms Verzögerung // Event-Listener für Klick auf Gedanken thoughtCard.addEventListener('click', (e) => { // Verhindern, dass der Link-Klick den Kartenklick auslöst if (e.target.tagName === 'A' || e.target.closest('a')) return; window.location.href = `/thoughts/${thought.id}`; }); // Hover-Animation für Karten thoughtCard.addEventListener('mouseenter', () => { thoughtCard.style.transform = 'translateY(-5px)'; thoughtCard.style.boxShadow = '0 10px 25px rgba(0, 0, 0, 0.2)'; }); thoughtCard.addEventListener('mouseleave', () => { thoughtCard.style.transform = 'translateY(0)'; thoughtCard.style.boxShadow = 'none'; }); container.appendChild(thoughtCard); }); } // Flash-Nachrichten mit Animationen showFlash(message, type = 'info') { if (!this.flashContainer) { this.flashContainer = document.createElement('div'); this.flashContainer.className = 'fixed top-4 right-4 z-50 flex flex-col gap-2'; document.body.appendChild(this.flashContainer); } const flash = document.createElement('div'); flash.className = `flash-message p-3 rounded-lg shadow-lg flex items-center gap-3 max-w-xs text-white`; // Verschiedene Stile je nach Typ let backgroundColor, icon; switch (type) { case 'success': backgroundColor = 'bg-green-500'; icon = 'fa-check-circle'; break; case 'error': backgroundColor = 'bg-red-500'; icon = 'fa-exclamation-circle'; break; case 'warning': backgroundColor = 'bg-yellow-500'; icon = 'fa-exclamation-triangle'; break; default: backgroundColor = 'bg-blue-500'; icon = 'fa-info-circle'; } flash.classList.add(backgroundColor); flash.innerHTML = `
${message}
`; // Füge den Flash zum Container hinzu this.flashContainer.appendChild(flash); // Einblend-Animation flash.style.opacity = '0'; flash.style.transform = 'translateX(20px)'; setTimeout(() => { flash.style.transition = 'all 0.3s ease'; flash.style.opacity = '1'; flash.style.transform = 'translateX(0)'; }, 10); // Schließen-Button const closeBtn = flash.querySelector('.flash-close'); closeBtn.addEventListener('click', () => { // Ausblend-Animation flash.style.opacity = '0'; flash.style.transform = 'translateX(20px)'; setTimeout(() => { flash.remove(); }, 300); }); // Automatisches Ausblenden nach 5 Sekunden setTimeout(() => { if (flash.parentNode) { flash.style.opacity = '0'; flash.style.transform = 'translateX(20px)'; setTimeout(() => { if (flash.parentNode) flash.remove(); }, 300); } }, 5000); return flash; } }