diff --git a/logs/app.log b/logs/app.log index 0ae7e59..7791ffc 100644 --- a/logs/app.log +++ b/logs/app.log @@ -425,3 +425,6 @@ werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on 2025-05-14 12:13:05,415 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:77] 2025-05-14 12:13:07,885 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:77] 2025-05-14 12:13:07,885 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:77] +2025-05-14 12:13:20,274 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:77] +2025-05-14 12:13:22,949 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:77] +2025-05-14 12:13:22,949 INFO: Anwendung gestartet [in C:\Users\TTOMCZA.EMEA\Dev\website\app.py:77] diff --git a/static/js/update_mindmap.js b/static/js/update_mindmap.js index 2d266f2..2e97f87 100644 --- a/static/js/update_mindmap.js +++ b/static/js/update_mindmap.js @@ -968,422 +968,237 @@ function handleEditingAction(action, nodeId) { } // Funktion zum Hinzufügen eines neuen Knotens -function addNewNode(cy) { - // Zentriere den neuen Knoten im sichtbaren Bereich - const extent = cy.extent(); - const centerX = (extent.x1 + extent.x2) / 2; - const centerY = (extent.y1 + extent.y2) / 2; - - // Generiere eine eindeutige ID - const newId = 'new-node-' + Date.now(); - - // Füge den neuen Knoten hinzu - cy.add({ - group: 'nodes', - data: { - id: newId, - label: 'Neuer Knoten', - category: 'Wissenschaft', - description: 'Beschreibung hinzufügen', - hasChildren: false, - expanded: false, - color: mindmapConfig.categories['Wissenschaft'].color, - fontColor: '#ffffff', - fontSize: 16, - neuronSize: 8, - neuronActivity: 0.8 - }, - position: { x: centerX, y: centerY } +function addNewNode(cy, parentNode = null) { + // Dialog erstellen + const dialog = document.createElement('div'); + dialog.className = 'node-dialog'; + dialog.style.position = 'absolute'; + dialog.style.top = '50%'; + dialog.style.left = '50%'; + dialog.style.transform = 'translate(-50%, -50%)'; + dialog.style.background = 'rgba(15, 23, 42, 0.95)'; + dialog.style.padding = '1.5rem'; + dialog.style.borderRadius = '0.75rem'; + dialog.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)'; + dialog.style.zIndex = '2000'; + dialog.style.width = '400px'; + dialog.style.maxWidth = '90vw'; + dialog.style.backdropFilter = 'blur(8px)'; + dialog.style.border = '1px solid rgba(255, 255, 255, 0.1)'; + + dialog.innerHTML = ` +

Neuen Knoten erstellen

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ `; + + document.body.appendChild(dialog); + + // Event-Listener + document.getElementById('cancel-node').addEventListener('click', () => { + document.body.removeChild(dialog); + }); + + document.getElementById('create-node').addEventListener('click', () => { + const name = document.getElementById('node-name').value || 'Neuer Knoten'; + const description = document.getElementById('node-description').value || ''; + const category = document.getElementById('node-category').value; + const color = document.getElementById('node-color').value; + + // Temporäre ID erstellen (wird später durch die echte ID ersetzt) + const tempId = 'new-' + Date.now(); + + // Knoten zur Visualisierung hinzufügen + const newNode = cy.add({ + group: 'nodes', + data: { + id: tempId, + label: name, + description: description, + category: category, + color: color, + icon: 'fa-solid fa-circle' + }, + position: { + x: cy.width() / 2 + (Math.random() * 100 - 50), + y: cy.height() / 2 + (Math.random() * 100 - 50) + } + }); + + // Wenn ein Elternknoten angegeben ist, eine Verbindung erstellen + if (parentNode) { + cy.add({ + group: 'edges', + data: { + source: parentNode.id(), + target: tempId, + strength: 0.5 + } + }); + } + + // Layout neu berechnen + cy.layout(mindmapStyles.layout.base).run(); + + document.body.removeChild(dialog); + showUINotification('Neuer Knoten erstellt!', 'success'); + }); + + // Kategorie-Auswahl mit Farben verknüpfen + document.getElementById('node-category').addEventListener('change', (e) => { + const category = e.target.value; + let color = '#9F7AEA'; // Standardfarbe + + switch(category) { + case 'Philosophie': + color = '#9F7AEA'; // Lila + break; + case 'Wissenschaft': + color = '#60A5FA'; // Blau + break; + case 'Technologie': + color = '#10B981'; // Grün + break; + case 'Künste': + color = '#F59E0B'; // Orange + break; + case 'Psychologie': + color = '#EF4444'; // Rot + break; + } + + document.getElementById('node-color').value = color; }); - - // Wähle den neuen Knoten aus, um ihn zu bearbeiten - const newNode = cy.getElementById(newId); - newNode.select(); - - // Öffne den Bearbeitungsdialog für den neuen Knoten - setTimeout(() => { - editNodeProperties(newNode); - }, 300); } -// Funktion zum Bearbeiten von Knoteneigenschaften +// Funktion zum Bearbeiten eines Knotens function editNodeProperties(node) { if (!node) return; - // Erstelle den Modal-Hintergrund - const backdrop = document.createElement('div'); - backdrop.className = 'modal-backdrop'; - document.body.appendChild(backdrop); - - // Erstelle den Bearbeitungsdialog - const dialog = document.createElement('div'); - dialog.className = 'node-edit-dialog'; - - // Hole die aktuellen Daten des Knotens const data = node.data(); - // Generiere die Kategorie-Optionen - let categoryOptions = ''; - for (const category in mindmapConfig.categories) { - categoryOptions += ``; - } - - // Erstelle das Formular + // Dialog erstellen + const dialog = document.createElement('div'); + dialog.className = 'node-dialog'; + dialog.style.position = 'absolute'; + dialog.style.top = '50%'; + dialog.style.left = '50%'; + dialog.style.transform = 'translate(-50%, -50%)'; + dialog.style.background = 'rgba(15, 23, 42, 0.95)'; + dialog.style.padding = '1.5rem'; + dialog.style.borderRadius = '0.75rem'; + dialog.style.boxShadow = '0 4px 20px rgba(0, 0, 0, 0.3)'; + dialog.style.zIndex = '2000'; + dialog.style.width = '400px'; + dialog.style.maxWidth = '90vw'; + dialog.style.backdropFilter = 'blur(8px)'; + dialog.style.border = '1px solid rgba(255, 255, 255, 0.1)'; + dialog.innerHTML = ` -

Knoten bearbeiten

-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
+

Knoten bearbeiten

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
`; - + document.body.appendChild(dialog); - - // Event-Listener für den Abbrechen-Button - dialog.querySelector('button.cancel').addEventListener('click', function() { + + // Event-Listener + document.getElementById('cancel-edit').addEventListener('click', () => { document.body.removeChild(dialog); - document.body.removeChild(backdrop); }); - - // Event-Listener für den Modal-Hintergrund - backdrop.addEventListener('click', function() { - document.body.removeChild(dialog); - document.body.removeChild(backdrop); - }); - - // Event-Listener für das Formular - dialog.querySelector('form').addEventListener('submit', function(e) { - e.preventDefault(); - - // Hole die neuen Werte aus dem Formular - const label = document.getElementById('node-label').value; + + document.getElementById('save-edit').addEventListener('click', () => { + const name = document.getElementById('node-name').value || 'Unbenannter Knoten'; + const description = document.getElementById('node-description').value || ''; const category = document.getElementById('node-category').value; - const description = document.getElementById('node-description').value; - const hasChildren = document.getElementById('node-has-children').value === 'true'; - - // Bestimme die Farbe basierend auf der Kategorie - const color = mindmapConfig.categories[category]?.color || data.color || '#60a5fa'; - - // Aktualisiere die Daten des Knotens + const color = document.getElementById('node-color').value; + + // Knoten aktualisieren node.data({ - ...data, - label, - category, - description, - hasChildren, - color - }); - - // Schließe den Dialog - document.body.removeChild(dialog); - document.body.removeChild(backdrop); - - // Benachrichtigung anzeigen - showUINotification('Knoten wurde aktualisiert', 'success'); - }); -} - -// Funktion zum Anzeigen des Kontext-Menüs für einen Knoten -function showNodeContextMenu(node, position) { - if (!node) return; - - // Entferne existierende Kontext-Menüs - const existingMenu = document.querySelector('.context-menu'); - if (existingMenu) { - existingMenu.parentNode.removeChild(existingMenu); - } - - // Erstelle das Kontext-Menü - const menu = document.createElement('div'); - menu.className = 'context-menu'; - menu.style.left = `${position.x}px`; - menu.style.top = `${position.y}px`; - - // Menü-Einträge - menu.innerHTML = ` -
- - - - - Bearbeiten -
-
- - - - - - Verbindung erstellen -
-
- - - - - Löschen -
- `; - - document.body.appendChild(menu); - - // Event-Listener für Menü-Einträge - menu.querySelectorAll('.context-menu-item').forEach(item => { - item.addEventListener('click', function() { - const action = this.getAttribute('data-action'); - - switch (action) { - case 'edit': - editNodeProperties(node); - break; - case 'connect': - startEdgeCreation(node); - break; - case 'delete': - deleteNode(node); - break; - } - - // Entferne das Menü - document.body.removeChild(menu); - }); - }); - - // Klick außerhalb des Menüs schließt es - document.addEventListener('click', function closeMenu(e) { - if (!menu.contains(e.target)) { - if (document.body.contains(menu)) { - document.body.removeChild(menu); - } - document.removeEventListener('click', closeMenu); - } - }); -} - -// Funktion zum Anzeigen des Menüs zum Hinzufügen eines Knotens -function showAddNodeMenu(position) { - // Entferne existierende Kontext-Menüs - const existingMenu = document.querySelector('.context-menu'); - if (existingMenu) { - existingMenu.parentNode.removeChild(existingMenu); - } - - // Erstelle das Kontext-Menü - const menu = document.createElement('div'); - menu.className = 'context-menu'; - menu.style.left = `${position.x}px`; - menu.style.top = `${position.y}px`; - - // Kategorie-Einträge für neue Knoten - let categoryItems = ''; - for (const category in mindmapConfig.categories) { - const categoryConfig = mindmapConfig.categories[category]; - categoryItems += ` -
- - ${category} -
- `; - } - - // Menü-Einträge - menu.innerHTML = ` -
Neuen Knoten hinzufügen
- ${categoryItems} - `; - - document.body.appendChild(menu); - - // Event-Listener für Kategorie-Einträge - menu.querySelectorAll('.context-menu-item').forEach(item => { - item.addEventListener('click', function() { - const category = this.getAttribute('data-category'); - - // Füge einen neuen Knoten an der Position hinzu - addNewNodeAtPosition(cy, position, category); - - // Entferne das Menü - document.body.removeChild(menu); - }); - }); - - // Klick außerhalb des Menüs schließt es - document.addEventListener('click', function closeMenu(e) { - if (!menu.contains(e.target)) { - if (document.body.contains(menu)) { - document.body.removeChild(menu); - } - document.removeEventListener('click', closeMenu); - } - }); -} - -// Funktion zum Hinzufügen eines neuen Knotens an einer bestimmten Position -function addNewNodeAtPosition(cy, position, category) { - if (!cy) return; - - // Berechne die Modellposition (statt der gerenderten Position) - const modelPosition = cy.renderer().projectIntoViewport(position.x, position.y); - - // Generiere eine eindeutige ID - const newId = 'new-node-' + Date.now(); - - // Hole die Kategorie-Konfiguration - const categoryConfig = mindmapConfig.categories[category] || mindmapConfig.categories['Wissenschaft']; - - // Füge den neuen Knoten hinzu - cy.add({ - group: 'nodes', - data: { - id: newId, - label: 'Neuer ' + category + '-Knoten', + label: name, + description: description, category: category, - description: categoryConfig.description || 'Beschreibung hinzufügen', - hasChildren: false, - expanded: false, - color: categoryConfig.color, - fontColor: '#ffffff', - fontSize: 16, - neuronSize: 8, - neuronActivity: 0.8 - }, - position: { - x: modelPosition[0], - y: modelPosition[1] - } - }); - - // Wähle den neuen Knoten aus, um ihn zu bearbeiten - const newNode = cy.getElementById(newId); - newNode.select(); - - // Öffne den Bearbeitungsdialog für den neuen Knoten - setTimeout(() => { - editNodeProperties(newNode); - }, 300); -} - -// Funktion zum Starten der Erstellung einer Kante -function startEdgeCreation(sourceNode) { - if (!sourceNode) return; - - const cy = sourceNode.cy(); - if (!cy) return; - - // Markiere den Quellknoten - sourceNode.addClass('edge-source'); - - // Füge dem Container die Klasse für den Kanten-Erstellungsmodus hinzu - cy.container().classList.add('edge-creation-mode'); - - // Zeige Hinweis an - showUINotification('Wähle einen Zielknoten für die Verbindung', 'info'); - - // Einmaliger Event-Listener für das Auswählen des Zielknotens - cy.once('tap', 'node', function(evt) { - const targetNode = evt.target; - - // Ignoriere, wenn es der gleiche Knoten ist - if (targetNode.id() === sourceNode.id()) { - showUINotification('Quell- und Zielknoten dürfen nicht identisch sein', 'warning'); - finishEdgeCreation(); - return; - } - - // Prüfe, ob bereits eine Kante existiert - const existingEdge = cy.edges(`[source="${sourceNode.id()}"][target="${targetNode.id()}"]`); - if (existingEdge.length > 0) { - showUINotification('Verbindung existiert bereits', 'warning'); - finishEdgeCreation(); - return; - } - - // Füge die neue Kante hinzu - cy.add({ - group: 'edges', - data: { - source: sourceNode.id(), - target: targetNode.id(), - strength: 0.5, - conductionVelocity: Math.random() * 0.5 + 0.3, - latency: Math.random() * 100 + 50 - } + color: color }); - - showUINotification('Verbindung wurde erstellt', 'success'); - finishEdgeCreation(); - }); - - // Event-Listener für Abbruch durch Klick auf leeren Bereich - cy.once('tap', function(evt) { - if (evt.target === cy) { - showUINotification('Kantenerstellung abgebrochen', 'info'); - finishEdgeCreation(); - } - }); - - function finishEdgeCreation() { - sourceNode.removeClass('edge-source'); - cy.container().classList.remove('edge-creation-mode'); - } -} -// Funktion zum Aktivieren des Kantenerstellungsmodus -function enableEdgeCreationMode(cy) { - if (!cy) return; - - // Füge dem Container die Klasse für den Kanten-Erstellungsmodus hinzu - cy.container().classList.add('edge-creation-mode'); - - // Zeige Hinweis an - showUINotification('Wähle einen Quellknoten für die Verbindung', 'info'); - - // Einmaliger Event-Listener für das Auswählen des Quellknotens - cy.once('tap', 'node', function(evt) { - const sourceNode = evt.target; - startEdgeCreation(sourceNode); + document.body.removeChild(dialog); + showUINotification('Knoten wurde aktualisiert!', 'success'); }); - - // Event-Listener für Abbruch durch Rechtsklick oder Escape-Taste - const cancelEdgeCreation = function() { - cy.container().classList.remove('edge-creation-mode'); - cy.removeListener('tap'); - showUINotification('Kantenerstellung abgebrochen', 'info'); - }; - - cy.once('cxttap', cancelEdgeCreation); - document.addEventListener('keydown', function escKeyHandler(e) { - if (e.key === 'Escape') { - cancelEdgeCreation(); - document.removeEventListener('keydown', escKeyHandler); + + // Kategorie-Auswahl mit Farben verknüpfen + document.getElementById('node-category').addEventListener('change', (e) => { + const category = e.target.value; + let color = '#9F7AEA'; // Standardfarbe + + switch(category) { + case 'Philosophie': + color = '#9F7AEA'; // Lila + break; + case 'Wissenschaft': + color = '#60A5FA'; // Blau + break; + case 'Technologie': + color = '#10B981'; // Grün + break; + case 'Künste': + color = '#F59E0B'; // Orange + break; + case 'Psychologie': + color = '#EF4444'; // Rot + break; } + + document.getElementById('node-color').value = color; }); } @@ -1391,130 +1206,275 @@ function enableEdgeCreationMode(cy) { function deleteNode(node) { if (!node) return; - // Frage den Benutzer, ob er den Knoten wirklich löschen möchte - if (confirm(`Möchtest du den Knoten "${node.data('label')}" wirklich löschen?`)) { - // Entferne den Knoten und alle zugehörigen Kanten + if (confirm(`Möchten Sie den Knoten "${node.data('label')}" wirklich löschen?`)) { + // Alle verbundenen Kanten löschen + node.connectedEdges().remove(); + // Knoten löschen node.remove(); - showUINotification('Knoten wurde gelöscht', 'success'); + showUINotification('Knoten wurde gelöscht!', 'success'); } } -// Funktion zum Löschen ausgewählter Elemente -function deleteSelectedElements(cy) { - if (!cy) return; +// Kontextmenü für Knoten anzeigen +function showNodeContextMenu(node, position) { + // Entferne vorhandene Kontextmenüs + removeContextMenus(); - const selectedElements = cy.elements(':selected'); - if (selectedElements.length === 0) { - showUINotification('Keine Elemente ausgewählt', 'warning'); - return; - } + const contextMenu = document.createElement('div'); + contextMenu.className = 'context-menu'; + contextMenu.style.left = `${position.x}px`; + contextMenu.style.top = `${position.y}px`; - // Zähle die ausgewählten Knoten und Kanten - const selectedNodes = selectedElements.filter('node'); - const selectedEdges = selectedElements.filter('edge'); + contextMenu.innerHTML = ` +
+ Bearbeiten +
+
+ Unterknoten hinzufügen +
+
+ Löschen +
+ `; - let confirmMessage = 'Möchtest du die ausgewählten Elemente wirklich löschen?'; - if (selectedNodes.length > 0 && selectedEdges.length > 0) { - confirmMessage = `Möchtest du ${selectedNodes.length} Knoten und ${selectedEdges.length} Verbindungen wirklich löschen?`; - } else if (selectedNodes.length > 0) { - confirmMessage = `Möchtest du ${selectedNodes.length} Knoten wirklich löschen?`; - } else if (selectedEdges.length > 0) { - confirmMessage = `Möchtest du ${selectedEdges.length} Verbindungen wirklich löschen?`; - } + document.body.appendChild(contextMenu); - // Frage den Benutzer, ob er die ausgewählten Elemente wirklich löschen möchte - if (confirm(confirmMessage)) { - // Entferne die ausgewählten Elemente - selectedElements.remove(); - - let successMessage = 'Elemente wurden gelöscht'; - if (selectedNodes.length > 0 && selectedEdges.length > 0) { - successMessage = `${selectedNodes.length} Knoten und ${selectedEdges.length} Verbindungen wurden gelöscht`; - } else if (selectedNodes.length > 0) { - successMessage = `${selectedNodes.length} Knoten wurden gelöscht`; - } else if (selectedEdges.length > 0) { - successMessage = `${selectedEdges.length} Verbindungen wurden gelöscht`; + // Event-Listener für Menüpunkte + contextMenu.querySelectorAll('.context-menu-item').forEach(item => { + item.addEventListener('click', function() { + const action = this.getAttribute('data-action'); + + switch (action) { + case 'edit': + editNodeProperties(node); + break; + case 'add-child': + addNewNode(window.cy, node); + break; + case 'delete': + deleteNode(node); + break; + } + + removeContextMenus(); + }); + }); + + // Event-Listener zum Schließen des Menüs + document.addEventListener('click', function closeMenu(e) { + if (!contextMenu.contains(e.target)) { + removeContextMenus(); + document.removeEventListener('click', closeMenu); } - - showUINotification(successMessage, 'success'); - } + }); } -// Funktion zum Speichern der Änderungen an der Mindmap -function saveMindmapChanges(cy, nodeId) { - if (!cy) return; +// Funktion zum Hinzufügen eines Knotens an einer bestimmten Position (Rechtsklick) +function showAddNodeMenu(position) { + // Entferne vorhandene Kontextmenüs + removeContextMenus(); - // Sammle die Daten aller Knoten und Kanten - const nodes = []; - const edges = []; + const contextMenu = document.createElement('div'); + contextMenu.className = 'context-menu'; + contextMenu.style.left = `${position.x}px`; + contextMenu.style.top = `${position.y}px`; - cy.nodes().forEach(node => { - const data = node.data(); - const position = node.position(); - - nodes.push({ - id: data.id, - name: data.label, - category: data.category, - description: data.description, - has_children: data.hasChildren, - color_code: data.color, - position_x: position.x, - position_y: position.y + contextMenu.innerHTML = ` +
+ Knoten hinzufügen +
+ `; + + document.body.appendChild(contextMenu); + + // Event-Listener für Menüpunkte + contextMenu.querySelectorAll('.context-menu-item').forEach(item => { + item.addEventListener('click', function() { + const action = this.getAttribute('data-action'); + + if (action === 'add-node') { + // Position im Canvas ermitteln + const containerPos = window.cy.container().getBoundingClientRect(); + const cyPos = { + x: position.x - containerPos.left, + y: position.y - containerPos.top + }; + + // Temporäre ID erstellen + const tempId = 'new-' + Date.now(); + + // Knoten hinzufügen (vereinfachte Version, zeigt direkt den Dialog an) + addNewNode(window.cy); + } + + removeContextMenus(); }); }); - cy.edges().forEach(edge => { - const data = edge.data(); - - edges.push({ - source: data.source, - target: data.target, - strength: data.strength - }); + // Event-Listener zum Schließen des Menüs + document.addEventListener('click', function closeMenu(e) { + if (!contextMenu.contains(e.target)) { + removeContextMenus(); + document.removeEventListener('click', closeMenu); + } }); +} + +// Hilfsfunktion zum Entfernen aller Kontextmenüs +function removeContextMenus() { + document.querySelectorAll('.context-menu').forEach(menu => { + menu.remove(); + }); +} + +// Funktion zum Aktivieren des Kanten-Erstellungsmodus +function enableEdgeCreationMode(cy) { + // Status anzeigen + showUINotification('Bitte wählen Sie den Startknoten für die Verbindung', 'info'); - // Erstelle die Daten für den API-Aufruf - const saveData = { - nodes: nodes, - edges: edges, - parent_id: nodeId || null + // Cursor-Stil ändern + document.body.style.cursor = 'crosshair'; + cy.container().classList.add('edge-creation-mode'); + + // Event-Handler für die Knotenauswahl + const nodeSelectHandler = function(event) { + // Ersten Knoten auswählen + const sourceNode = event.target; + + // Status aktualisieren + showUINotification('Wählen Sie jetzt den Zielknoten für die Verbindung', 'info'); + + // Zweiten Knoten abwarten + const secondNodeHandler = function(event) { + const targetNode = event.target; + + // Prüfen, ob der Zielknoten ein Knoten ist und nicht derselbe wie der Startknoten + if (targetNode.isNode() && targetNode.id() !== sourceNode.id()) { + // Kante erstellen + cy.add({ + group: 'edges', + data: { + source: sourceNode.id(), + target: targetNode.id(), + strength: 0.5 + } + }); + + // Erfolgsmeldung + showUINotification('Verbindung erstellt!', 'success'); + + // Event-Handler entfernen + cy.off('tap', 'node', secondNodeHandler); + + // Modus beenden + finishEdgeCreation(); + } + }; + + // Event-Handler für den zweiten Knoten + cy.on('tap', 'node', secondNodeHandler); + + // Original-Handler entfernen + cy.off('tap', 'node', nodeSelectHandler); + + // ESC-Taste zum Abbrechen + document.addEventListener('keydown', function escKeyHandler(e) { + if (e.key === 'Escape') { + cy.off('tap', 'node', secondNodeHandler); + finishEdgeCreation(); + document.removeEventListener('keydown', escKeyHandler); + showUINotification('Verbindungserstellung abgebrochen', 'info'); + } + }); }; - console.log('Speichere Mindmap-Änderungen:', saveData); + // Event-Handler für den ersten Knoten + cy.on('tap', 'node', nodeSelectHandler); - // Zeige eine Speicherbenachrichtigung an - showUINotification('Speichere Mindmap-Änderungen...', 'info'); + // Funktion zum Beenden des Kantenerstellungsmodus + function finishEdgeCreation() { + document.body.style.cursor = ''; + cy.container().classList.remove('edge-creation-mode'); + } - // Sende die Daten an den Server - fetch('/api/mindmap/save', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(saveData) - }) - .then(response => { - if (!response.ok) { - return response.json().then(data => { - throw new Error(data.error || `Fehler ${response.status}: ${response.statusText}`); - }); + // Abbrechen durch Klick außerhalb + cy.on('tap', function(event) { + if (event.target === cy) { + cy.off('tap', 'node', nodeSelectHandler); + finishEdgeCreation(); + showUINotification('Verbindungserstellung abgebrochen', 'info'); } - return response.json(); - }) - .then(data => { - console.log('Speichern erfolgreich:', data); - showUINotification('Mindmap wurde erfolgreich gespeichert', 'success'); - }) - .catch(error => { - console.error('Fehler beim Speichern der Mindmap:', error); - showUINotification({ - error: 'Fehler beim Speichern der Mindmap', - details: error.message - }, 'error'); }); } +// Funktion zum Speichern von Mindmap-Änderungen +async function saveMindmapChanges(cy) { + try { + showUINotification('Speichere Änderungen...', 'info'); + + // Alle Knoten und Kanten sammeln + const nodes = cy.nodes().map(node => { + const data = node.data(); + return { + id: data.id, + name: data.label, + description: data.description || '', + color_code: data.color || data.color_code, + category: data.category || null, + icon: data.icon || 'fa-solid fa-circle', + position_x: node.position().x, + position_y: node.position().y + }; + }); + + const edges = cy.edges().map(edge => { + return { + source: edge.source().id(), + target: edge.target().id(), + strength: edge.data('strength') || 0.5 + }; + }); + + // Daten an API senden + const response = await fetch('/api/mindmap/save', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + nodes: nodes, + edges: edges + }) + }); + + const result = await response.json(); + + if (!response.ok || !result.success) { + const errorMessage = result.error || 'Mindmap konnte nicht gespeichert werden'; + showUINotification(errorMessage, 'error'); + throw new Error(errorMessage); + } + + // Wenn temporäre IDs aktualisiert wurden, Mapping anwenden + if (result.node_mapping) { + // Die echten IDs hinzufügen + for (const [tempId, realId] of Object.entries(result.node_mapping)) { + const node = cy.getElementById(tempId); + if (node.length > 0) { + node.data('id', realId); + } + } + } + + showUINotification('Mindmap wurde erfolgreich gespeichert!', 'success'); + return result; + } catch (error) { + console.error('Fehler beim Speichern der Mindmap:', error); + showUINotification('Fehler beim Speichern: ' + error.message, 'error'); + throw error; + } +} + // Füge CSS-Stile für den Bearbeitungsmodus hinzu const editingStyles = document.createElement('style'); editingStyles.textContent = ` diff --git a/templates/mindmap.html b/templates/mindmap.html index ceef8e1..84cbf50 100644 --- a/templates/mindmap.html +++ b/templates/mindmap.html @@ -31,6 +31,9 @@ backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255, 255, 255, 0.1); z-index: 10; + display: flex; + justify-content: space-between; + align-items: center; } .mindmap-title { @@ -43,6 +46,47 @@ -webkit-text-fill-color: transparent; } + /* Aktionsmenü im Header */ + .mindmap-actions { + display: flex; + gap: 0.75rem; + } + + .action-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: rgba(255, 255, 255, 0.1); + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.3s ease; + } + + .action-button:hover { + background: rgba(255, 255, 255, 0.2); + } + + .action-button.primary { + background: rgba(139, 92, 246, 0.3); + } + + .action-button.primary:hover { + background: rgba(139, 92, 246, 0.5); + } + + .action-button.danger { + background: rgba(220, 38, 38, 0.3); + } + + .action-button.danger:hover { + background: rgba(220, 38, 38, 0.5); + } + /* Kontrollpanel */ .control-panel { position: absolute; @@ -79,6 +123,85 @@ margin-right: 0.75rem; } + /* CRUD Panel */ + .crud-panel { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + background: rgba(15, 23, 42, 0.9); + border-radius: 1rem; + padding: 1rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + z-index: 10; + border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + gap: 0.75rem; + backdrop-filter: blur(8px); + } + + .crud-button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 4rem; + height: 4rem; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.75rem; + color: white; + cursor: pointer; + transition: all 0.3s ease; + } + + .crud-button:hover { + background: rgba(255, 255, 255, 0.2); + transform: translateY(-5px); + } + + .crud-button i { + font-size: 1.25rem; + margin-bottom: 0.25rem; + } + + .crud-button span { + font-size: 0.7rem; + text-align: center; + } + + .crud-button.create { + background: rgba(16, 185, 129, 0.3); + } + + .crud-button.create:hover { + background: rgba(16, 185, 129, 0.5); + } + + .crud-button.edit { + background: rgba(245, 158, 11, 0.3); + } + + .crud-button.edit:hover { + background: rgba(245, 158, 11, 0.5); + } + + .crud-button.delete { + background: rgba(220, 38, 38, 0.3); + } + + .crud-button.delete:hover { + background: rgba(220, 38, 38, 0.5); + } + + .crud-button.save { + background: rgba(59, 130, 246, 0.3); + } + + .crud-button.save:hover { + background: rgba(59, 130, 246, 0.5); + } + /* Info-Panel */ .info-panel { position: absolute; @@ -194,6 +317,53 @@ text-align: center; max-width: 80%; } + + /* Bearbeitungsmodus-Hinweis */ + .edit-mode-indicator { + position: fixed; + bottom: 1rem; + left: 1rem; + background: rgba(245, 158, 11, 0.8); + color: white; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-size: 0.9rem; + z-index: 1000; + display: none; + } + + .edit-mode-indicator.active { + display: flex; + align-items: center; + gap: 0.5rem; + } + + /* Kontext-Menü */ + .context-menu { + position: absolute; + background: rgba(30, 41, 59, 0.95); + border-radius: 8px; + padding: 8px 0; + min-width: 160px; + z-index: 2000; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.1); + } + + .context-menu-item { + padding: 8px 16px; + color: white; + cursor: pointer; + transition: background-color 0.2s ease; + display: flex; + align-items: center; + gap: 8px; + } + + .context-menu-item:hover { + background: rgba(255, 255, 255, 0.1); + } {% endblock %} @@ -202,6 +372,22 @@

Interaktive Wissenslandkarte

+ + +
+ + + +
@@ -209,6 +395,12 @@ + + +
+ + Bearbeitungsmodus +
@@ -233,6 +425,30 @@ + + +

Knotendetails

@@ -281,6 +497,23 @@ document.addEventListener('DOMContentLoaded', function() { const cyContainer = document.getElementById('cy'); const loader = document.getElementById('loader'); const statusMessage = document.getElementById('statusMessage'); + const crudPanel = document.getElementById('crudPanel'); + const editModeIndicator = document.getElementById('editModeIndicator'); + + // CRUD Buttons + const createNodeBtn = document.getElementById('createNode'); + const createEdgeBtn = document.getElementById('createEdge'); + const editNodeBtn = document.getElementById('editNode'); + const deleteElementBtn = document.getElementById('deleteElement'); + const saveMindmapBtn = document.getElementById('saveMindmap'); + + // Header Action Buttons + const toggleEditModeBtn = document.getElementById('toggleEditMode'); + const saveChangesBtn = document.getElementById('saveChanges'); + const cancelEditBtn = document.getElementById('cancelEdit'); + + let isEditMode = false; + let selectedElement = null; if (cyContainer) { console.log('Container gefunden:', cyContainer); @@ -299,6 +532,56 @@ document.addEventListener('DOMContentLoaded', function() { // Erfolg: Loader und Statusmeldung ausblenden loader.style.display = 'none'; statusMessage.style.display = 'none'; + + // Event-Listener für Knotenauswahl + window.cy.on('select', 'node', function(event) { + selectedElement = event.target; + editNodeBtn.disabled = false; + deleteElementBtn.disabled = false; + + // Knotendetails im Info-Panel anzeigen + showNodeInfo(selectedElement); + }); + + window.cy.on('select', 'edge', function(event) { + selectedElement = event.target; + editNodeBtn.disabled = true; + deleteElementBtn.disabled = false; + }); + + window.cy.on('unselect', function() { + selectedElement = null; + editNodeBtn.disabled = true; + deleteElementBtn.disabled = true; + + // Info-Panel ausblenden + hideNodeInfo(); + }); + + // Rechtsklick-Menü + window.cy.on('cxttap', 'node', function(event) { + // Kontextmenü für Knoten anzeigen + if (isEditMode) { + const node = event.target; + const position = event.renderedPosition; + showNodeContextMenu(node, { + x: event.originalEvent.clientX, + y: event.originalEvent.clientY + }); + event.preventDefault(); + } + }); + + window.cy.on('cxttap', function(event) { + // Kontextmenü zum Hinzufügen eines Knotens + if (isEditMode && event.target === window.cy) { + showAddNodeMenu({ + x: event.originalEvent.clientX, + y: event.originalEvent.clientY + }); + event.preventDefault(); + } + }); }).catch(error => { // Fehler: Fehlermeldung anzeigen console.error('Mindmap-Initialisierung fehlgeschlagen', error); @@ -318,6 +601,96 @@ document.addEventListener('DOMContentLoaded', function() { console.error('Container #cy nicht gefunden'); } + // Bearbeitungsmodus umschalten + toggleEditModeBtn.addEventListener('click', function() { + isEditMode = !isEditMode; + + if (isEditMode) { + // Bearbeitungsmodus aktivieren + crudPanel.style.display = 'flex'; + editModeIndicator.classList.add('active'); + toggleEditModeBtn.style.display = 'none'; + saveChangesBtn.style.display = 'inline-flex'; + cancelEditBtn.style.display = 'inline-flex'; + window.cy.container().classList.add('editing-mode'); + + // Aktiviere Knotenbewegung (dragging) + window.cy.nodes().unlock(); + } else { + // Bearbeitungsmodus deaktivieren + crudPanel.style.display = 'none'; + editModeIndicator.classList.remove('active'); + toggleEditModeBtn.style.display = 'inline-flex'; + saveChangesBtn.style.display = 'none'; + cancelEditBtn.style.display = 'none'; + window.cy.container().classList.remove('editing-mode'); + + // Deaktiviere Knotenbewegung + window.cy.nodes().lock(); + } + }); + + // Änderungen speichern + saveChangesBtn.addEventListener('click', function() { + saveMindmapChanges(window.cy); + }); + + // Bearbeitungsmodus abbrechen + cancelEditBtn.addEventListener('click', function() { + if (confirm('Möchten Sie den Bearbeitungsmodus wirklich verlassen? Nicht gespeicherte Änderungen gehen verloren.')) { + isEditMode = false; + crudPanel.style.display = 'none'; + editModeIndicator.classList.remove('active'); + toggleEditModeBtn.style.display = 'inline-flex'; + saveChangesBtn.style.display = 'none'; + cancelEditBtn.style.display = 'none'; + window.cy.container().classList.remove('editing-mode'); + + // Neuinitialisierung der Mindmap + initializeMindmap().then(() => { + loader.style.display = 'none'; + statusMessage.style.display = 'none'; + }); + } + }); + + // CRUD-Funktionen + createNodeBtn.addEventListener('click', function() { + if (isEditMode) { + addNewNode(window.cy); + } + }); + + createEdgeBtn.addEventListener('click', function() { + if (isEditMode) { + enableEdgeCreationMode(window.cy); + } + }); + + editNodeBtn.addEventListener('click', function() { + if (isEditMode && selectedElement && selectedElement.isNode()) { + editNodeProperties(selectedElement); + } + }); + + deleteElementBtn.addEventListener('click', function() { + if (isEditMode && selectedElement) { + if (selectedElement.isNode()) { + deleteNode(selectedElement); + } else if (selectedElement.isEdge()) { + if (confirm('Möchten Sie diese Verbindung wirklich löschen?')) { + selectedElement.remove(); + } + } + } + }); + + saveMindmapBtn.addEventListener('click', function() { + if (isEditMode) { + saveMindmapChanges(window.cy); + } + }); + // Zoom-Funktionen document.getElementById('zoomIn').addEventListener('click', function() { if (window.cy) window.cy.zoom(window.cy.zoom() * 1.2); @@ -335,6 +708,36 @@ document.addEventListener('DOMContentLoaded', function() { const legend = document.getElementById('categoryLegend'); legend.style.display = legend.style.display === 'none' ? 'flex' : 'none'; }); + + // Funktionen für Knoteninfo-Panel + function showNodeInfo(node) { + if (!node || !node.isNode()) return; + + const infoPanel = document.getElementById('infoPanel'); + const infoTitle = infoPanel.querySelector('.info-title'); + const infoContent = infoPanel.querySelector('.info-content'); + + const data = node.data(); + + infoTitle.textContent = data.label || 'Knotendetails'; + + let contentHTML = ` +

Kategorie: ${data.category || 'Nicht kategorisiert'}

+

Beschreibung: ${data.description || 'Keine Beschreibung verfügbar'}

+ `; + + if (data.hasChildren) { + contentHTML += `

Hat Unterknoten

`; + } + + infoContent.innerHTML = contentHTML; + infoPanel.classList.add('visible'); + } + + function hideNodeInfo() { + const infoPanel = document.getElementById('infoPanel'); + infoPanel.classList.remove('visible'); + } }); {% endblock %} \ No newline at end of file