572 lines
17 KiB
JavaScript
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;
|
|
}
|