Files
website/static/js/mindmap-interaction.js

572 lines
17 KiB
JavaScript

/**
* Mindmap Interaction Enhancement
* Verbessert die Interaktion mit der Mindmap und steuert die Seitenleisten-Anzeige
*/
// Stellt sicher, dass das Dokument geladen ist, bevor Aktionen ausgeführt werden
document.addEventListener('DOMContentLoaded', function() {
console.log('Mindmap-Interaktionsverbesserungen werden initialisiert...');
// Auf das Laden der Mindmap warten
document.addEventListener('mindmap-loaded', setupInteractionEnhancements);
// Sofortiges Setup für statische Interaktionen
setupStaticInteractions();
// Direkten Event-Listener für Knotenauswahl einrichten
setupNodeSelectionListener();
// Neuronales Netzwerk-Hintergrund-Effekt aktivieren
setupNeuralNetworkEffect();
});
// Erzeugt subtile Hintergrundeffekte für neuronales Netzwerk
function setupNeuralNetworkEffect() {
const cyContainer = document.getElementById('cy');
if (!cyContainer) return;
// Dendrite Animation CSS
const dendriteStyle = document.createElement('style');
dendriteStyle.textContent = `
.neuron-pulse {
position: absolute;
border-radius: 50%;
background: radial-gradient(circle, rgba(139, 92, 246, 0.1) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
opacity: 0;
animation: neuronPulse 6s ease-in-out infinite;
transform: translate(-50%, -50%);
}
@keyframes neuronPulse {
0%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
50% { opacity: 0.8; transform: translate(-50%, -50%) scale(1.2); }
}
.synapse-line {
position: absolute;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(139, 92, 246, 0.3), transparent);
pointer-events: none;
z-index: 0;
opacity: 0;
transform-origin: 0% 50%;
animation: synapseFlow 8s ease-in-out infinite;
}
@keyframes synapseFlow {
0%, 100% { opacity: 0; }
50% { opacity: 0.5; }
}
`;
document.head.appendChild(dendriteStyle);
// Zufällige Pulsierende Dendrite-Effekte
setInterval(() => {
if (Math.random() > 0.7) {
const pulse = document.createElement('div');
pulse.className = 'neuron-pulse';
// Zufällige Größe und Position
const size = Math.random() * 200 + 100;
pulse.style.width = `${size}px`;
pulse.style.height = `${size}px`;
pulse.style.left = `${Math.random() * 100}%`;
pulse.style.top = `${Math.random() * 100}%`;
// Animation-Eigenschaften variieren
pulse.style.animationDuration = `${Math.random() * 4 + 3}s`;
pulse.style.animationDelay = `${Math.random() * 2}s`;
cyContainer.appendChild(pulse);
// Element nach Animation entfernen
setTimeout(() => pulse.remove(), 7000);
}
// Zufällige Synapse-Linien-Effekte
if (Math.random() > 0.8) {
const synapse = document.createElement('div');
synapse.className = 'synapse-line';
// Zufällige Position und Größe
const startX = Math.random() * 100;
const startY = Math.random() * 100;
const length = Math.random() * 200 + 50;
const angle = Math.random() * 360;
synapse.style.width = `${length}px`;
synapse.style.left = `${startX}%`;
synapse.style.top = `${startY}%`;
synapse.style.transform = `rotate(${angle}deg)`;
// Animation-Eigenschaften
synapse.style.animationDuration = `${Math.random() * 3 + 5}s`;
synapse.style.animationDelay = `${Math.random() * 2}s`;
cyContainer.appendChild(synapse);
// Element nach Animation entfernen
setTimeout(() => synapse.remove(), 9000);
}
}, 800);
}
// Richtet grundlegende statische Interaktionen ein
function setupStaticInteractions() {
// Initialisiert die Hover-Effekte für die Seitenleisten-Panels
initializePanelEffects();
// Prevent default Zoom bei CTRL + Mausrad
document.addEventListener('wheel', function(e) {
if (e.ctrlKey) {
e.preventDefault();
}
}, { passive: false });
// Initialisiert verbesserten Zoom-Handler direkt nach dem Laden
initializeZoomHandler();
}
// Richtet erweiterte Interaktionen mit der geladenen Mindmap ein
function setupInteractionEnhancements() {
console.log('Mindmap geladen - verbesserte Interaktionen werden eingerichtet');
// Cytoscape-Instanz
const cy = window.cy;
if (!cy) {
console.warn('Cytoscape-Instanz nicht gefunden!');
return;
}
// Hover-Effekte für Knoten
cy.on('mouseover', 'node', function(evt) {
const node = evt.target;
// Nur anwenden, wenn der Knoten nicht ausgewählt ist
if (!node.selected()) {
node.style({
'shadow-opacity': 0.8,
'shadow-blur': 'mapData(neuronActivity, 0.3, 1, 10, 20)',
'background-opacity': 1
});
}
// Verbundene Kanten hervorheben
node.connectedEdges().style({
'line-opacity': 0.7,
'width': 'mapData(strength, 0.2, 0.8, 1.5, 2.5)'
});
});
cy.on('mouseout', 'node', function(evt) {
const node = evt.target;
// Nur zurücksetzen, wenn nicht ausgewählt
if (!node.selected()) {
node.removeStyle();
}
// Verbundene Kanten zurücksetzen, wenn nicht mit ausgewähltem Knoten verbunden
node.connectedEdges().forEach(edge => {
const sourceSelected = edge.source().selected();
const targetSelected = edge.target().selected();
if (!sourceSelected && !targetSelected) {
edge.removeStyle();
}
});
});
// Verhindere, dass der Browser die Seite scrollt, wenn über der Mindmap gezoomt wird
preventScrollWhileZooming();
// Tastaturkürzel für Mindmap-Interaktionen
setupKeyboardShortcuts(cy);
}
// Initialisiert speziellen Zoom-Handler für sanften Zoom
function initializeZoomHandler() {
// Auf das Laden der Mindmap warten
document.addEventListener('mindmap-loaded', function() {
if (!window.cy) return;
const cy = window.cy;
// Laufenden AnimationsFrame-Request speichern
let zoomAnimationFrame = null;
let targetZoom = cy.zoom();
let currentZoom = targetZoom;
let zoomCenter = { x: 0, y: 0 };
let zoomTime = 0;
// Aktuellen Zoom überwachen und sanft anpassen
function updateZoom() {
// Sanfter Übergang zum Ziel-Zoom-Level
zoomTime += 0.08;
// Easing-Funktion für flüssigere Bewegung
const easedProgress = 1 - Math.pow(1 - Math.min(zoomTime, 1), 3);
if (currentZoom !== targetZoom) {
currentZoom += (targetZoom - currentZoom) * easedProgress;
// Zoom mit Position anwenden
cy.zoom({
level: currentZoom,
position: zoomCenter
});
// Loop fortsetzen, bis wir sehr nahe am Ziel sind
if (Math.abs(currentZoom - targetZoom) > 0.001 && zoomTime < 1) {
zoomAnimationFrame = requestAnimationFrame(updateZoom);
} else {
// Endgültigen Zoom setzen, um sicherzustellen, dass wir genau das Ziel erreichen
cy.zoom({
level: targetZoom,
position: zoomCenter
});
zoomAnimationFrame = null;
}
} else {
zoomAnimationFrame = null;
}
}
// Überschreibe den Standard-mousewheel-Handler von Cytoscape
cy.removeAllListeners('mousewheel');
cy.on('mousewheel', function(e) {
e.preventDefault();
const delta = e.originalEvent.deltaY;
const mousePosition = e.position || e.cyPosition;
// Glätten und Limitieren des Zoom-Faktors
const factor = delta > 0 ? 0.97 : 1.03;
// Neues Zoom-Level berechnen mit Begrenzung
const maxZoom = cy.maxZoom() || 3;
const minZoom = cy.minZoom() || 0.2;
targetZoom = Math.min(maxZoom, Math.max(minZoom, cy.zoom() * factor));
// Position für Zoom setzen
zoomCenter = mousePosition;
// Zeit zurücksetzen
zoomTime = 0;
// Laufende Animation abbrechen und neue starten
if (zoomAnimationFrame) {
cancelAnimationFrame(zoomAnimationFrame);
}
zoomAnimationFrame = requestAnimationFrame(updateZoom);
});
// Panning auch flüssiger gestalten
cy.on('pan', function() {
cy.style().selector('node').style({
'transition-property': 'none',
}).update();
});
cy.on('panend', function() {
cy.style().selector('node').style({
'transition-property': 'background-color, shadow-color, shadow-opacity, shadow-blur',
'transition-duration': '0.3s'
}).update();
});
});
}
// Verhindert Browser-Scrolling während Zoom in der Mindmap
function preventScrollWhileZooming() {
const cyContainer = document.getElementById('cy');
if (cyContainer) {
cyContainer.addEventListener('wheel', function(e) {
// Verhindern des Standard-Scrollens während des Zooms
e.preventDefault();
}, { passive: false });
}
}
// Initialisiert Effekte für Seitenleisten-Panels
function initializePanelEffects() {
// Selektiert alle Panel-Elemente
const panels = document.querySelectorAll('.sidebar-panel');
panels.forEach(panel => {
// Hover-Effekt für Panels
panel.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-5px)';
this.style.boxShadow = '0 10px 25px rgba(0, 0, 0, 0.3), 0 0 15px rgba(139, 92, 246, 0.3)';
});
panel.addEventListener('mouseleave', function() {
this.style.transform = '';
this.style.boxShadow = '';
});
});
}
// Richtet Tastaturkürzel für Mindmap-Interaktionen ein
function setupKeyboardShortcuts(cy) {
document.addEventListener('keydown', function(e) {
// Nur fortfahren, wenn keine Texteingabe im Fokus ist
if (document.activeElement.tagName === 'INPUT' ||
document.activeElement.tagName === 'TEXTAREA' ||
document.activeElement.isContentEditable) {
return;
}
// Tastaturkürzel
switch(e.key) {
case '+':
case '=':
// Einzoomen (sanfter)
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
smoothZoom(cy, 1.15, 400);
}
break;
case '-':
// Auszoomen (sanfter)
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
smoothZoom(cy, 0.85, 400);
}
break;
case '0':
// Zoom auf Gesamtansicht
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
smoothFit(cy);
}
break;
case 'Escape':
// Ausgewählten Knoten abwählen
cy.nodes().unselect();
resetNodeSelection();
break;
}
});
}
// Sanften Zoom mit Animation anwenden
function smoothZoom(cy, factor, duration = 400) {
const currentZoom = cy.zoom();
const targetZoom = currentZoom * factor;
// Mittelpunkt der Ansicht verwenden
const center = {
x: cy.width() / 2,
y: cy.height() / 2
};
// Sanftes Zoomen mit Animation
cy.animation({
zoom: {
level: targetZoom,
position: center
},
duration: duration,
easing: 'cubic-bezier(0.19, 1, 0.22, 1)'
}).play();
}
// Sanftes Anpassen der Ansicht mit Animation
function smoothFit(cy, padding = 50) {
cy.animation({
fit: {
eles: cy.elements(),
padding: padding
},
duration: 600,
easing: 'cubic-bezier(0.19, 1, 0.22, 1)'
}).play();
}
// Richtet den Event-Listener für die Knotenauswahl ein
function setupNodeSelectionListener() {
document.addEventListener('mindmap-loaded', function() {
if (!window.cy) return;
window.cy.on('tap', 'node', function(evt) {
handleNodeSelection(evt.target);
});
window.cy.on('tap', function(evt) {
if (evt.target === window.cy) {
resetNodeSelection();
}
});
});
}
// Verarbeitet die Knotenauswahl
function handleNodeSelection(node) {
if (!node) return;
// Stelle sicher, dass nur dieser Knoten ausgewählt ist
window.cy.nodes().unselect();
node.select();
// Speichere ausgewählten Knoten global
window.mindmapInstance.selectedNode = node;
// Zeige Informationen in der Sidebar an
showNodeDescriptionSidebar(node);
// Informationspanel aktualisieren
updateNodeInfoPanel(node);
// Node zentrieren mit Animation
window.cy.animation({
center: {
eles: node
},
zoom: 1.3,
duration: 800,
easing: 'cubic-bezier(0.19, 1, 0.22, 1)'
}).play();
// Hervorhebung für den ausgewählten Knoten mit Leuchteffekt
node.style({
'background-opacity': 1,
'shadow-opacity': 1,
'shadow-blur': 20,
'shadow-color': node.data('color')
});
// Verbindungen hervorheben
node.connectedEdges().style({
'line-color': '#a78bfa',
'line-opacity': 0.7,
'width': 2,
'line-style': 'solid'
});
}
// Setzt die Knotenauswahl zurück
function resetNodeSelection() {
const nodeInfoPanel = document.getElementById('node-info-panel');
if (nodeInfoPanel) {
nodeInfoPanel.classList.remove('visible');
}
showDefaultSidebar();
// Globale Referenz zurücksetzen
if (window.mindmapInstance) {
window.mindmapInstance.selectedNode = null;
}
}
// Zeigt das Informationspanel für einen Knoten an
function updateNodeInfoPanel(node) {
const nodeInfoPanel = document.getElementById('node-info-panel');
const nodeDescription = document.getElementById('node-description');
const connectedNodes = document.getElementById('connected-nodes');
const panelTitle = nodeInfoPanel ? nodeInfoPanel.querySelector('.info-panel-title') : null;
if (!nodeInfoPanel || !nodeDescription || !connectedNodes || !panelTitle) return;
// Titel und Beschreibung aktualisieren
panelTitle.textContent = node.data('name');
nodeDescription.textContent = node.data('description') || 'Keine Beschreibung verfügbar.';
// Verbundene Knoten auflisten
connectedNodes.innerHTML = '';
// Verbundene Knoten ermitteln
const connectedNodesList = [];
node.connectedEdges().forEach(edge => {
let connectedNode;
if (edge.source().id() === node.id()) {
connectedNode = edge.target();
} else {
connectedNode = edge.source();
}
if (!connectedNodesList.some(n => n.id() === connectedNode.id())) {
connectedNodesList.push(connectedNode);
}
});
// Verbundene Knoten im Panel anzeigen
if (connectedNodesList.length > 0) {
connectedNodesList.forEach(connectedNode => {
const nodeLink = document.createElement('span');
nodeLink.className = 'inline-block px-2 py-1 text-xs rounded-md m-1 cursor-pointer opacity-80 hover:opacity-100 transition-opacity';
nodeLink.style.backgroundColor = connectedNode.data('color');
nodeLink.textContent = connectedNode.data('name');
// Beim Klick auf den verbundenen Knoten zu diesem navigieren
nodeLink.addEventListener('click', function() {
handleNodeSelection(connectedNode);
});
connectedNodes.appendChild(nodeLink);
});
} else {
connectedNodes.innerHTML = '<span class="text-sm italic">Keine verbundenen Knoten</span>';
}
// Panel anzeigen mit Animation
nodeInfoPanel.classList.add('visible');
}
// Zeigt die Standard-Seitenleiste an
function showDefaultSidebar() {
// Alle Seitenleisten-Panels anzeigen/ausblenden
const allPanels = document.querySelectorAll('[data-sidebar]');
allPanels.forEach(panel => {
if (panel.getAttribute('data-sidebar') === 'node-description') {
panel.classList.add('hidden');
} else {
panel.classList.remove('hidden');
}
});
}
// Zeigt die Knotenbeschreibung in der Seitenleiste an
function showNodeDescriptionSidebar(node) {
// Das Knotenbeschreibungs-Panel finden
const nodeDescPanel = document.querySelector('[data-sidebar="node-description"]');
if (nodeDescPanel) {
// Panel sichtbar machen
nodeDescPanel.classList.remove('hidden');
// Titel und Beschreibung aktualisieren
const nodeTitleElement = nodeDescPanel.querySelector('[data-node-title]');
const nodeDescElement = nodeDescPanel.querySelector('[data-node-description]');
if (nodeTitleElement) {
nodeTitleElement.textContent = node.data('name');
}
if (nodeDescElement) {
// Beschreibung mit HTML-Formatierung anzeigen
nodeDescElement.innerHTML = generateNodeDescription(node);
}
}
}
// Generiert eine formatierte HTML-Beschreibung für einen Knoten
function generateNodeDescription(node) {
let description = node.data('description') || 'Keine Beschreibung verfügbar.';
// Einfache HTML-Formatierung (kann erweitert werden)
description = description
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>');
return description;
}